DefaultTypeConverter.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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 Jint.Extensions;
  8. using Jint.Native;
  9. using Jint.Native.Function;
  10. using Jint.Native.Object;
  11. using Jint.Runtime.Descriptors;
  12. using Expression = System.Linq.Expressions.Expression;
  13. #pragma warning disable IL2026
  14. #pragma warning disable IL2067
  15. #pragma warning disable IL2070
  16. #pragma warning disable IL2072
  17. #pragma warning disable IL3050
  18. namespace Jint.Runtime.Interop
  19. {
  20. public class DefaultTypeConverter : ITypeConverter
  21. {
  22. private readonly Engine _engine;
  23. private readonly record struct TypeConversionKey(Type Source, Type Target);
  24. private static readonly ConcurrentDictionary<TypeConversionKey, MethodInfo?> _knownCastOperators = new();
  25. private static readonly Type intType = typeof(int);
  26. private static readonly Type iCallableType = typeof(Func<JsValue, JsValue[], JsValue>);
  27. private static readonly Type jsValueType = typeof(JsValue);
  28. private static readonly Type objectType = typeof(object);
  29. private static readonly Type engineType = typeof(Engine);
  30. private static readonly Type typeType = typeof(Type);
  31. private static readonly MethodInfo convertChangeType = typeof(Convert).GetMethod("ChangeType", new[] { objectType, typeType, typeof(IFormatProvider) })!;
  32. private static readonly MethodInfo jsValueFromObject = jsValueType.GetMethod(nameof(JsValue.FromObject))!;
  33. private static readonly MethodInfo jsValueToObject = jsValueType.GetMethod(nameof(JsValue.ToObject))!;
  34. public DefaultTypeConverter(Engine engine)
  35. {
  36. _engine = engine;
  37. }
  38. public virtual object? Convert(
  39. object? value,
  40. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] Type type,
  41. IFormatProvider formatProvider)
  42. {
  43. if (!TryConvert(value, type, formatProvider, propagateException: true, out var converted, out var problemMessage))
  44. {
  45. ExceptionHelper.ThrowError(_engine, problemMessage ?? $"Unable to convert {value} to type {type}");
  46. }
  47. return converted;
  48. }
  49. public virtual bool TryConvert(
  50. object? value,
  51. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] Type type,
  52. IFormatProvider formatProvider,
  53. [NotNullWhen(true)] out object? converted)
  54. {
  55. return TryConvert(value, type, formatProvider, propagateException: false, out converted, out _);
  56. }
  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. var integer = System.Convert.ChangeType(value, intType, formatProvider);
  98. if (integer == null)
  99. {
  100. ExceptionHelper.ThrowArgumentOutOfRangeException();
  101. }
  102. converted = Enum.ToObject(type, integer);
  103. return true;
  104. }
  105. var valueType = value.GetType();
  106. // is the javascript value an ICallable instance ?
  107. if (valueType == iCallableType)
  108. {
  109. if (typeof(Delegate).IsAssignableFrom(type) && !type.IsAbstract)
  110. {
  111. // use target function instance as cache holder, this way delegate and target hold same lifetime
  112. var delegatePropertyKey = "__jint_delegate_" + type.GUID;
  113. var func = (Func<JsValue, JsValue[], JsValue>) value;
  114. var functionInstance = func.Target as Function;
  115. var d = functionInstance?.GetHiddenClrObjectProperty(delegatePropertyKey) as Delegate;
  116. if (d is null)
  117. {
  118. d = BuildDelegate(type, func);
  119. functionInstance?.SetHiddenClrObjectProperty(delegatePropertyKey, d);
  120. }
  121. converted = d;
  122. return true;
  123. }
  124. }
  125. if (type.IsArray)
  126. {
  127. var source = value as object[];
  128. if (source == null)
  129. {
  130. problemMessage = $"Value of object[] type is expected, but actual type is {value.GetType()}";
  131. return false;
  132. }
  133. var targetElementType = type.GetElementType()!;
  134. var itemsConverted = new object?[source.Length];
  135. for (var i = 0; i < source.Length; i++)
  136. {
  137. itemsConverted[i] = Convert(source[i], targetElementType, formatProvider);
  138. }
  139. var result = Array.CreateInstance(targetElementType, source.Length);
  140. itemsConverted.CopyTo(result, 0);
  141. converted = result;
  142. return true;
  143. }
  144. var typeDescriptor = TypeDescriptor.Get(valueType);
  145. if (typeDescriptor.IsStringKeyedGenericDictionary)
  146. {
  147. // public empty constructor required
  148. var constructors = type.GetConstructors();
  149. // value types
  150. if (type.IsValueType && constructors.Length > 0)
  151. {
  152. problemMessage = $"No valid constructors found for {type}";
  153. return false;
  154. }
  155. var constructorParameters = Array.Empty<object>();
  156. // reference types - return null if no valid constructor is found
  157. if (!type.IsValueType)
  158. {
  159. var found = false;
  160. foreach (var constructor in constructors)
  161. {
  162. if (constructor.GetParameters().Length == 0 && constructor.IsPublic)
  163. {
  164. found = true;
  165. break;
  166. }
  167. }
  168. if (!found)
  169. {
  170. foreach (var constructor in constructors)
  171. {
  172. var parameterInfos = constructor.GetParameters();
  173. if (parameterInfos.All(static p => p.IsOptional) && constructor.IsPublic)
  174. {
  175. constructorParameters = new object[parameterInfos.Length];
  176. found = true;
  177. break;
  178. }
  179. }
  180. }
  181. if (!found)
  182. {
  183. problemMessage = $"No valid constructors found for type {type}";
  184. return false;
  185. }
  186. }
  187. var obj = Activator.CreateInstance(type, constructorParameters)!;
  188. var members = type.GetMembers();
  189. foreach (var member in members)
  190. {
  191. // only use fields an properties
  192. if (member.MemberType != MemberTypes.Property &&
  193. member.MemberType != MemberTypes.Field)
  194. {
  195. continue;
  196. }
  197. if (typeDescriptor.TryGetValue(value, member.Name, out var val)
  198. || typeDescriptor.TryGetValue(value, member.Name.UpperToLowerCamelCase(), out val))
  199. {
  200. var output = Convert(val, member.GetDefinedType(), formatProvider);
  201. member.SetValue(obj, output);
  202. }
  203. }
  204. converted = obj;
  205. return true;
  206. }
  207. try
  208. {
  209. converted = System.Convert.ChangeType(value, type, formatProvider);
  210. return true;
  211. }
  212. catch (Exception e)
  213. {
  214. // check if we can do a cast with operator overloading
  215. if (TryCastWithOperators(value, type, valueType, out var invoke))
  216. {
  217. converted = invoke;
  218. return true;
  219. }
  220. if (propagateException && !_engine.Options.Interop.ExceptionHandler(e))
  221. {
  222. throw;
  223. }
  224. problemMessage = e.Message;
  225. return false;
  226. }
  227. }
  228. private Delegate BuildDelegate(
  229. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type,
  230. Func<JsValue, JsValue[], JsValue> function)
  231. {
  232. var method = type.GetMethod("Invoke");
  233. var arguments = method!.GetParameters();
  234. var parameters = new ParameterExpression[arguments.Length];
  235. for (var i = 0; i < parameters.Length; i++)
  236. {
  237. parameters[i] = Expression.Parameter(arguments[i].ParameterType, arguments[i].Name);
  238. }
  239. var initializers = new List<MethodCallExpression>(parameters.Length);
  240. for (var i = 0; i < parameters.Length; i++)
  241. {
  242. var param = parameters[i];
  243. if (param.Type.IsValueType)
  244. {
  245. var boxing = Expression.Convert(param, objectType);
  246. initializers.Add(Expression.Call(null, jsValueFromObject, Expression.Constant(_engine, engineType), boxing));
  247. }
  248. else if (param.Type.IsArray &&
  249. arguments[i].GetCustomAttribute<ParamArrayAttribute>() is not null &&
  250. function.Target is Function instance)
  251. {
  252. for (var j = 0; j < instance.GetLength(); j++)
  253. {
  254. var returnLabel = Expression.Label(typeof(object));
  255. var checkIndex = Expression.GreaterThanOrEqual(Expression.Property(param, nameof(Array.Length)), Expression.Constant(j));
  256. var condition = Expression.IfThen(checkIndex, Expression.Return(returnLabel, Expression.ArrayAccess(param, Expression.Constant(j))));
  257. var block = Expression.Block(condition, Expression.Label(returnLabel, Expression.Constant(JsValue.Undefined)));
  258. initializers.Add(Expression.Call(null, jsValueFromObject, Expression.Constant(_engine, engineType), block));
  259. }
  260. }
  261. else
  262. {
  263. initializers.Add(Expression.Call(null, jsValueFromObject, Expression.Constant(_engine, engineType), param));
  264. }
  265. }
  266. var vars = Expression.NewArrayInit(jsValueType, initializers);
  267. var callExpression = Expression.Call(
  268. Expression.Constant(function.Target),
  269. function.Method,
  270. Expression.Constant(JsValue.Undefined, jsValueType),
  271. vars);
  272. if (method.ReturnType != typeof(void))
  273. {
  274. return Expression.Lambda(
  275. type,
  276. Expression.Convert(
  277. Expression.Call(
  278. null,
  279. convertChangeType,
  280. Expression.Call(callExpression, jsValueToObject),
  281. Expression.Constant(method.ReturnType),
  282. Expression.Constant(System.Globalization.CultureInfo.InvariantCulture, typeof(IFormatProvider))
  283. ),
  284. method.ReturnType
  285. ),
  286. new ReadOnlyCollection<ParameterExpression>(parameters)).Compile();
  287. }
  288. return Expression.Lambda(
  289. type,
  290. callExpression,
  291. new ReadOnlyCollection<ParameterExpression>(parameters)).Compile();
  292. }
  293. private static bool TryCastWithOperators(object value, Type type, Type valueType, [NotNullWhen(true)] out object? converted)
  294. {
  295. var key = new TypeConversionKey(valueType, type);
  296. static MethodInfo? CreateValueFactory(TypeConversionKey k)
  297. {
  298. var (source, target) = k;
  299. foreach (var m in source.GetOperatorOverloadMethods().Concat(target.GetOperatorOverloadMethods()))
  300. {
  301. if (!target.IsAssignableFrom(m.ReturnType) || m.Name is not ("op_Implicit" or "op_Explicit"))
  302. {
  303. continue;
  304. }
  305. var parameters = m.GetParameters();
  306. if (parameters.Length != 1 || !parameters[0].ParameterType.IsAssignableFrom(source))
  307. {
  308. continue;
  309. }
  310. // we found a match
  311. return m;
  312. }
  313. return null;
  314. }
  315. var castOperator = _knownCastOperators.GetOrAdd(key, CreateValueFactory);
  316. if (castOperator != null)
  317. {
  318. try
  319. {
  320. converted = castOperator.Invoke(null, new[] { value });
  321. return converted is not null;
  322. }
  323. catch
  324. {
  325. converted = null;
  326. return false;
  327. }
  328. }
  329. converted = null;
  330. return false;
  331. }
  332. }
  333. internal static class ObjectExtensions
  334. {
  335. public static object? GetHiddenClrObjectProperty(this ObjectInstance obj, string name)
  336. {
  337. return (obj.Get(name) as IObjectWrapper)?.Target;
  338. }
  339. public static void SetHiddenClrObjectProperty(this ObjectInstance obj, string name, object value)
  340. {
  341. obj.SetOwnProperty(name, new PropertyDescriptor(ObjectWrapper.Create(obj.Engine, value), PropertyFlag.AllForbidden));
  342. }
  343. }
  344. }