DefaultTypeConverter.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. using System.Collections.Concurrent;
  2. using System.Collections.ObjectModel;
  3. using System.Diagnostics.CodeAnalysis;
  4. using System.Linq;
  5. using System.Linq.Expressions;
  6. using System.Reflection;
  7. using System.Runtime.CompilerServices;
  8. using Jint.Extensions;
  9. using Jint.Native;
  10. using Jint.Native.Function;
  11. using Expression = System.Linq.Expressions.Expression;
  12. #pragma warning disable IL2026
  13. #pragma warning disable IL2067
  14. #pragma warning disable IL2070
  15. #pragma warning disable IL2072
  16. #pragma warning disable IL3050
  17. namespace Jint.Runtime.Interop;
  18. public class DefaultTypeConverter : ITypeConverter
  19. {
  20. private readonly Engine _engine;
  21. private readonly record struct TypeConversionKey(Type Source, Type Target);
  22. private static readonly ConcurrentDictionary<TypeConversionKey, MethodInfo?> _knownCastOperators = new();
  23. private static readonly Type intType = typeof(int);
  24. private static readonly Type iCallableType = typeof(JsCallDelegate);
  25. private static readonly Type jsValueType = typeof(JsValue);
  26. private static readonly Type objectType = typeof(object);
  27. private static readonly Type engineType = typeof(Engine);
  28. private static readonly MethodInfo changeTypeIfConvertible = typeof(DefaultTypeConverter).GetMethod(
  29. nameof(ChangeTypeOnlyIfConvertible), BindingFlags.NonPublic | BindingFlags.Static)!;
  30. private static readonly MethodInfo jsValueFromObject = jsValueType.GetMethod(nameof(JsValue.FromObject))!;
  31. private static readonly MethodInfo jsValueToObject = jsValueType.GetMethod(nameof(JsValue.ToObject))!;
  32. public DefaultTypeConverter(Engine engine)
  33. {
  34. _engine = engine;
  35. }
  36. public virtual object? Convert(
  37. object? value,
  38. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] Type type,
  39. IFormatProvider formatProvider)
  40. {
  41. if (!TryConvert(value, type, formatProvider, propagateException: true, out var converted, out var problemMessage))
  42. {
  43. Throw.Error(_engine, problemMessage ?? $"Unable to convert {value} to type {type}");
  44. }
  45. return converted;
  46. }
  47. public virtual bool TryConvert(
  48. object? value,
  49. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] Type type,
  50. IFormatProvider formatProvider,
  51. [NotNullWhen(true)] out object? converted)
  52. {
  53. return TryConvert(value, type, formatProvider, propagateException: false, out converted, out _);
  54. }
  55. private static readonly ConditionalWeakTable<IFunction, Func<object, Delegate>> _targetBinderDelegateCache = new();
  56. private static readonly ConditionalWeakTable<object, Delegate> _boundTargetDelegateCache = new();
  57. private bool TryConvert(
  58. object? value,
  59. [DynamicallyAccessedMembers(InteropHelper.DefaultDynamicallyAccessedMemberTypes)] Type type,
  60. IFormatProvider formatProvider,
  61. bool propagateException,
  62. out object? converted,
  63. out string? problemMessage)
  64. {
  65. converted = null;
  66. problemMessage = null;
  67. if (value is null)
  68. {
  69. if (InteropHelper.TypeIsNullable(type))
  70. {
  71. return true;
  72. }
  73. problemMessage = $"Unable to convert null to '{type.FullName}'";
  74. return false;
  75. }
  76. // don't try to convert if value is derived from type
  77. if (type.IsInstanceOfType(value))
  78. {
  79. converted = value;
  80. return true;
  81. }
  82. if (type.IsGenericType)
  83. {
  84. var result = InteropHelper.IsAssignableToGenericType(value.GetType(), type);
  85. if (result.IsAssignable)
  86. {
  87. converted = value;
  88. return true;
  89. }
  90. }
  91. if (type.IsNullable())
  92. {
  93. type = Nullable.GetUnderlyingType(type)!;
  94. }
  95. if (type.IsEnum)
  96. {
  97. if (EnumTryParse(type, value.ToString(), out converted))
  98. {
  99. return true;
  100. }
  101. }
  102. var valueType = value.GetType();
  103. // is the javascript value an ICallable instance ?
  104. if (valueType == iCallableType)
  105. {
  106. if (typeof(Delegate).IsAssignableFrom(type) && !type.IsAbstract)
  107. {
  108. var func = (JsCallDelegate) value;
  109. var functionInstance = func.Target;
  110. // caching of .NET delegates per function instance is required to be able to support
  111. // unregistering event handlers (see ShouldExecuteActionCallbackOnEventChanged)
  112. var d = functionInstance is not null ?
  113. _boundTargetDelegateCache.GetValue(functionInstance!, target =>
  114. {
  115. var astFunction = (functionInstance as Function)?._functionDefinition?.Function;
  116. // use a single builder per unique function AST
  117. var targetBinder = astFunction is not null
  118. ? _targetBinderDelegateCache.GetValue(astFunction, _ => BuildTargetBinderDelegate(type, func))
  119. : BuildTargetBinderDelegate(type, func);
  120. return targetBinder(target)!;
  121. }) :
  122. BuildDelegate(type, func, Expression.Constant(functionInstance, functionInstance!.GetType())).Compile();
  123. converted = d;
  124. return true;
  125. }
  126. }
  127. if (type.IsArray)
  128. {
  129. if (value is not object[] source)
  130. {
  131. problemMessage = $"Value of object[] type is expected, but actual type is {value.GetType()}";
  132. return false;
  133. }
  134. var targetElementType = type.GetElementType()!;
  135. var itemsConverted = new object?[source.Length];
  136. for (var i = 0; i < source.Length; i++)
  137. {
  138. itemsConverted[i] = Convert(source[i], targetElementType, formatProvider);
  139. }
  140. var result = Array.CreateInstance(targetElementType, source.Length);
  141. itemsConverted.CopyTo(result, 0);
  142. converted = result;
  143. return true;
  144. }
  145. var typeDescriptor = TypeDescriptor.Get(valueType);
  146. if (typeDescriptor.IsStringKeyedGenericDictionary)
  147. {
  148. // public empty constructor required
  149. var constructors = type.GetConstructors();
  150. // value types
  151. if (type.IsValueType && constructors.Length > 0)
  152. {
  153. problemMessage = $"No valid constructors found for {type}";
  154. return false;
  155. }
  156. var constructorParameters = Array.Empty<object>();
  157. // reference types - return null if no valid constructor is found
  158. if (!type.IsValueType)
  159. {
  160. var found = false;
  161. foreach (var constructor in constructors)
  162. {
  163. if (constructor.GetParameters().Length == 0 && constructor.IsPublic)
  164. {
  165. found = true;
  166. break;
  167. }
  168. }
  169. if (!found)
  170. {
  171. foreach (var constructor in constructors)
  172. {
  173. var parameterInfos = constructor.GetParameters();
  174. if (Array.TrueForAll(parameterInfos, static p => p.IsOptional) && constructor.IsPublic)
  175. {
  176. constructorParameters = new object[parameterInfos.Length];
  177. found = true;
  178. break;
  179. }
  180. }
  181. }
  182. if (!found)
  183. {
  184. problemMessage = $"No valid constructors found for type {type}";
  185. return false;
  186. }
  187. }
  188. var obj = Activator.CreateInstance(type, constructorParameters)!;
  189. var members = type.GetMembers();
  190. foreach (var member in members)
  191. {
  192. // only use fields an properties
  193. if (member.MemberType != MemberTypes.Property &&
  194. member.MemberType != MemberTypes.Field)
  195. {
  196. continue;
  197. }
  198. if (typeDescriptor.TryGetValue(value, member.Name, out var val)
  199. || typeDescriptor.TryGetValue(value, member.Name.UpperToLowerCamelCase(), out val))
  200. {
  201. var output = Convert(val, member.GetDefinedType(), formatProvider);
  202. member.SetValue(obj, output);
  203. }
  204. }
  205. converted = obj;
  206. return true;
  207. }
  208. try
  209. {
  210. converted = System.Convert.ChangeType(value, type, formatProvider);
  211. return true;
  212. }
  213. catch (Exception e)
  214. {
  215. // check if we can do a cast with operator overloading
  216. if (TryCastWithOperators(value, type, valueType, out var invoke))
  217. {
  218. converted = invoke;
  219. return true;
  220. }
  221. if (propagateException && !_engine.Options.Interop.ExceptionHandler(e))
  222. {
  223. throw;
  224. }
  225. problemMessage = e.Message;
  226. return false;
  227. }
  228. }
  229. private static bool EnumTryParse(Type enumType, string? value, [NotNullWhen(true)] out object? result)
  230. {
  231. if (value is null)
  232. {
  233. result = null;
  234. return false;
  235. }
  236. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
  237. return Enum.TryParse(enumType, value, ignoreCase: false, out result!);
  238. #else
  239. try
  240. {
  241. result = Enum.Parse(enumType, value, ignoreCase: false);
  242. return true;
  243. }
  244. catch (ArgumentException)
  245. {
  246. result = null!;
  247. return false;
  248. }
  249. #endif
  250. }
  251. private Func<object, Delegate> BuildTargetBinderDelegate(
  252. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type delegateType,
  253. JsCallDelegate function)
  254. {
  255. // Parameter for the target object
  256. var targetParam = Expression.Parameter(typeof(object), "target");
  257. var castedTarget = Expression.Convert(targetParam, function.Target!.GetType());
  258. var innerDelegate = BuildDelegate(delegateType, function, castedTarget);
  259. // Create the outer delegate: Func<object, Delegate>
  260. var outerDelegateType = typeof(Func<object, Delegate>);
  261. var curried = Expression.Lambda(
  262. outerDelegateType,
  263. innerDelegate,
  264. targetParam);
  265. return (Func<object, Delegate>) curried.Compile();
  266. }
  267. private LambdaExpression BuildDelegate(
  268. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type,
  269. JsCallDelegate function,
  270. Expression targetExpression)
  271. {
  272. var method = type.GetMethod("Invoke");
  273. var arguments = method!.GetParameters();
  274. var parameters = new ParameterExpression[arguments.Length];
  275. for (var i = 0; i < parameters.Length; i++)
  276. {
  277. parameters[i] = Expression.Parameter(arguments[i].ParameterType, arguments[i].Name);
  278. }
  279. var initializers = new List<MethodCallExpression>(parameters.Length);
  280. for (var i = 0; i < parameters.Length; i++)
  281. {
  282. var param = parameters[i];
  283. if (param.Type.IsValueType)
  284. {
  285. var boxing = Expression.Convert(param, objectType);
  286. initializers.Add(Expression.Call(null, jsValueFromObject, Expression.Constant(_engine, engineType), boxing));
  287. }
  288. else if (param.Type.IsArray &&
  289. arguments[i].GetCustomAttribute<ParamArrayAttribute>() is not null &&
  290. function.Target is Function instance)
  291. {
  292. for (var j = 0; j < instance.GetLength(); j++)
  293. {
  294. var returnLabel = Expression.Label(typeof(object));
  295. var checkIndex = Expression.GreaterThanOrEqual(Expression.Property(param, nameof(Array.Length)), Expression.Constant(j));
  296. var condition = Expression.IfThen(checkIndex, Expression.Return(returnLabel, Expression.ArrayAccess(param, Expression.Constant(j))));
  297. var block = Expression.Block(condition, Expression.Label(returnLabel, Expression.Constant(JsValue.Undefined)));
  298. initializers.Add(Expression.Call(null, jsValueFromObject, Expression.Constant(_engine, engineType), block));
  299. }
  300. }
  301. else
  302. {
  303. initializers.Add(Expression.Call(null, jsValueFromObject, Expression.Constant(_engine, engineType), param));
  304. }
  305. }
  306. var vars = Expression.NewArrayInit(jsValueType, initializers);
  307. var callExpression = Expression.Call(
  308. targetExpression,
  309. function.Method,
  310. Expression.Constant(JsValue.Undefined, jsValueType),
  311. vars);
  312. if (method.ReturnType != typeof(void))
  313. {
  314. return Expression.Lambda(
  315. type,
  316. Expression.Convert(
  317. Expression.Call(
  318. null,
  319. changeTypeIfConvertible,
  320. Expression.Call(callExpression, jsValueToObject),
  321. Expression.Constant(method.ReturnType),
  322. Expression.Constant(System.Globalization.CultureInfo.InvariantCulture, typeof(IFormatProvider))
  323. ),
  324. method.ReturnType
  325. ),
  326. new ReadOnlyCollection<ParameterExpression>(parameters));
  327. }
  328. return Expression.Lambda(
  329. type,
  330. callExpression,
  331. new ReadOnlyCollection<ParameterExpression>(parameters));
  332. }
  333. [return: NotNullIfNotNull(nameof(value))]
  334. private static object? ChangeTypeOnlyIfConvertible(object? value, Type conversionType, IFormatProvider? provider)
  335. {
  336. if (value == null || value is IConvertible)
  337. return System.Convert.ChangeType(value, conversionType, provider);
  338. return value;
  339. }
  340. private static bool TryCastWithOperators(object value, Type type, Type valueType, [NotNullWhen(true)] out object? converted)
  341. {
  342. var key = new TypeConversionKey(valueType, type);
  343. static MethodInfo? CreateValueFactory(TypeConversionKey k)
  344. {
  345. var (source, target) = k;
  346. foreach (var m in source.GetOperatorOverloadMethods().Concat(target.GetOperatorOverloadMethods()))
  347. {
  348. if (!target.IsAssignableFrom(m.ReturnType) || m.Name is not ("op_Implicit" or "op_Explicit"))
  349. {
  350. continue;
  351. }
  352. var parameters = m.GetParameters();
  353. if (parameters.Length != 1 || !parameters[0].ParameterType.IsAssignableFrom(source))
  354. {
  355. continue;
  356. }
  357. // we found a match
  358. return m;
  359. }
  360. return null;
  361. }
  362. var castOperator = _knownCastOperators.GetOrAdd(key, CreateValueFactory);
  363. if (castOperator != null)
  364. {
  365. try
  366. {
  367. converted = castOperator.Invoke(null, [value]);
  368. return converted is not null;
  369. }
  370. catch
  371. {
  372. converted = null;
  373. return false;
  374. }
  375. }
  376. converted = null;
  377. return false;
  378. }
  379. }