DelegateWrapper.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. using System.Globalization;
  2. using System.Reflection;
  3. using Jint.Extensions;
  4. using Jint.Native;
  5. using Jint.Native.Function;
  6. namespace Jint.Runtime.Interop
  7. {
  8. /// <summary>
  9. /// Represents a FunctionInstance wrapper around a CLR method. This is used by user to pass
  10. /// custom methods to the engine.
  11. /// </summary>
  12. public sealed class DelegateWrapper : Function
  13. {
  14. private static readonly JsString _name = new JsString("delegate");
  15. private readonly Delegate _d;
  16. private readonly bool _delegateContainsParamsArgument;
  17. public DelegateWrapper(
  18. Engine engine, Delegate d)
  19. : base(engine, engine.Realm, _name, FunctionThisMode.Global)
  20. {
  21. _d = d;
  22. _prototype = engine.Realm.Intrinsics.Function.PrototypeObject;
  23. var parameterInfos = _d.Method.GetParameters();
  24. _delegateContainsParamsArgument = false;
  25. foreach (var p in parameterInfos)
  26. {
  27. if (Attribute.IsDefined(p, typeof(ParamArrayAttribute)))
  28. {
  29. _delegateContainsParamsArgument = true;
  30. break;
  31. }
  32. }
  33. }
  34. protected internal override JsValue Call(JsValue thisObject, JsValue[] arguments)
  35. {
  36. var parameterInfos = _d.Method.GetParameters();
  37. #if NETFRAMEWORK
  38. if (parameterInfos.Length > 0 && parameterInfos[0].ParameterType == typeof(System.Runtime.CompilerServices.Closure))
  39. {
  40. var reducedLength = parameterInfos.Length - 1;
  41. var reducedParameterInfos = new ParameterInfo[reducedLength];
  42. Array.Copy(parameterInfos, 1, reducedParameterInfos, 0, reducedLength);
  43. parameterInfos = reducedParameterInfos;
  44. }
  45. #endif
  46. int delegateArgumentsCount = parameterInfos.Length;
  47. int delegateNonParamsArgumentsCount = _delegateContainsParamsArgument ? delegateArgumentsCount - 1 : delegateArgumentsCount;
  48. int jsArgumentsCount = arguments.Length;
  49. int jsArgumentsWithoutParamsCount = Math.Min(jsArgumentsCount, delegateNonParamsArgumentsCount);
  50. var clrTypeConverter = Engine.TypeConverter;
  51. var valueCoercionType = Engine.Options.Interop.ValueCoercion;
  52. var parameters = new object?[delegateArgumentsCount];
  53. // convert non params parameter to expected types
  54. for (var i = 0; i < jsArgumentsWithoutParamsCount; i++)
  55. {
  56. var parameterType = parameterInfos[i].ParameterType;
  57. var value = arguments[i];
  58. object? converted;
  59. if (parameterType == typeof(JsValue))
  60. {
  61. converted = value;
  62. }
  63. else if (!ReflectionExtensions.TryConvertViaTypeCoercion(parameterType, valueCoercionType, value, out converted))
  64. {
  65. converted = clrTypeConverter.Convert(
  66. value.ToObject(),
  67. parameterType,
  68. CultureInfo.InvariantCulture);
  69. }
  70. parameters[i] = converted;
  71. }
  72. // assign null to parameters not provided
  73. for (var i = jsArgumentsWithoutParamsCount; i < delegateNonParamsArgumentsCount; i++)
  74. {
  75. if (parameterInfos[i].ParameterType.IsValueType)
  76. {
  77. parameters[i] = Activator.CreateInstance(parameterInfos[i].ParameterType);
  78. }
  79. else
  80. {
  81. parameters[i] = null;
  82. }
  83. }
  84. // assign params to array and converts each object to expected type
  85. if (_delegateContainsParamsArgument)
  86. {
  87. int paramsArgumentIndex = delegateArgumentsCount - 1;
  88. int paramsCount = Math.Max(0, jsArgumentsCount - delegateNonParamsArgumentsCount);
  89. var paramsParameterType = parameterInfos[paramsArgumentIndex].ParameterType.GetElementType();
  90. var paramsParameter = Array.CreateInstance(paramsParameterType!, paramsCount);
  91. for (var i = paramsArgumentIndex; i < jsArgumentsCount; i++)
  92. {
  93. var paramsIndex = i - paramsArgumentIndex;
  94. var value = arguments[i];
  95. object? converted;
  96. if (paramsParameterType == typeof(JsValue))
  97. {
  98. converted = value;
  99. }
  100. else if (!ReflectionExtensions.TryConvertViaTypeCoercion(paramsParameterType, valueCoercionType, value, out converted))
  101. {
  102. converted = Engine.TypeConverter.Convert(
  103. value.ToObject(),
  104. paramsParameterType!,
  105. CultureInfo.InvariantCulture);
  106. }
  107. paramsParameter.SetValue(converted, paramsIndex);
  108. }
  109. parameters[paramsArgumentIndex] = paramsParameter;
  110. }
  111. try
  112. {
  113. var result = _d.DynamicInvoke(parameters);
  114. if (!IsAwaitable(result))
  115. {
  116. return FromObject(Engine, result);
  117. }
  118. return ConvertAwaitableToPromise(result!);
  119. }
  120. catch (TargetInvocationException exception)
  121. {
  122. ExceptionHelper.ThrowMeaningfulException(Engine, exception);
  123. throw;
  124. }
  125. }
  126. private static bool IsAwaitable(object? obj)
  127. {
  128. if (obj is null)
  129. {
  130. return false;
  131. }
  132. if (obj is Task)
  133. {
  134. return true;
  135. }
  136. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
  137. if (obj is ValueTask)
  138. {
  139. return true;
  140. }
  141. // ValueTask<T> is not derived from ValueTask, so we need to check for it explicitly
  142. var type = obj.GetType();
  143. if (!type.IsGenericType)
  144. {
  145. return false;
  146. }
  147. return type.GetGenericTypeDefinition() == typeof(ValueTask<>);
  148. #else
  149. return false;
  150. #endif
  151. }
  152. private JsValue ConvertAwaitableToPromise(object obj)
  153. {
  154. if (obj is Task task)
  155. {
  156. return ConvertTaskToPromise(task);
  157. }
  158. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
  159. if (obj is ValueTask valueTask)
  160. {
  161. return ConvertTaskToPromise(valueTask.AsTask());
  162. }
  163. // ValueTask<T>
  164. var asTask = obj.GetType().GetMethod(nameof(ValueTask<object>.AsTask));
  165. if (asTask is not null)
  166. {
  167. return ConvertTaskToPromise((Task) asTask.Invoke(obj, parameters: null)!);
  168. }
  169. #endif
  170. return FromObject(Engine, JsValue.Undefined);
  171. }
  172. private JsValue ConvertTaskToPromise(Task task)
  173. {
  174. var (promise, resolve, reject) = Engine.RegisterPromise();
  175. task = task.ContinueWith(continuationAction =>
  176. {
  177. if (continuationAction.IsFaulted)
  178. {
  179. reject(FromObject(Engine, continuationAction.Exception));
  180. }
  181. else if (continuationAction.IsCanceled)
  182. {
  183. reject(FromObject(Engine, new ExecutionCanceledException()));
  184. }
  185. else
  186. {
  187. // Special case: Marshal `async Task` as undefined, as this is `Task<VoidTaskResult>` at runtime
  188. // See https://github.com/sebastienros/jint/pull/1567#issuecomment-1681987702
  189. if (Task.CompletedTask.Equals(continuationAction))
  190. {
  191. resolve(FromObject(Engine, JsValue.Undefined));
  192. return;
  193. }
  194. var result = continuationAction.GetType().GetProperty(nameof(Task<object>.Result));
  195. if (result is not null)
  196. {
  197. resolve(FromObject(Engine, result.GetValue(continuationAction)));
  198. }
  199. else
  200. {
  201. resolve(FromObject(Engine, JsValue.Undefined));
  202. }
  203. }
  204. });
  205. Engine.AddToEventLoop(() =>
  206. {
  207. if (!task.IsCompleted)
  208. {
  209. // Task.Wait has the potential of inlining the task's execution on the current thread; avoid this.
  210. ((IAsyncResult) task).AsyncWaitHandle.WaitOne();
  211. }
  212. });
  213. return promise;
  214. }
  215. }
  216. }