You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

202 lines
10 KiB

  1. using IPA.Logging;
  2. using IPA.Utilities;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Linq.Expressions;
  8. #if NET4
  9. using Task = System.Threading.Tasks.Task;
  10. using TaskEx = System.Threading.Tasks.Task;
  11. using Expression = System.Linq.Expressions.Expression;
  12. using ExpressionEx = System.Linq.Expressions.Expression;
  13. #endif
  14. #if NET3
  15. using System.Threading.Tasks;
  16. using Net3_Proxy;
  17. using Path = Net3_Proxy.Path;
  18. using File = Net3_Proxy.File;
  19. using Directory = Net3_Proxy.Directory;
  20. #endif
  21. namespace IPA.Loader
  22. {
  23. // NOTE: TaskEx.WhenAll() (Task.WhenAll() in .NET 4) returns CompletedTask if it has no arguments, which we need for .NET 3
  24. internal class PluginExecutor
  25. {
  26. public PluginMetadata Metadata { get; }
  27. public PluginExecutor(PluginMetadata meta, bool isSelf)
  28. {
  29. Metadata = meta;
  30. if (isSelf)
  31. {
  32. CreatePlugin = m => null;
  33. LifecycleEnable = o => { };
  34. LifecycleDisable = o => TaskEx.WhenAll();
  35. }
  36. else
  37. PrepareDelegates();
  38. }
  39. public object Instance { get; private set; } = null;
  40. private Func<PluginMetadata, object> CreatePlugin { get; set; }
  41. private Action<object> LifecycleEnable { get; set; }
  42. // disable may be async (#24)
  43. private Func<object, Task> LifecycleDisable { get; set; }
  44. public void Create()
  45. {
  46. if (Instance != null) return;
  47. Instance = CreatePlugin(Metadata);
  48. }
  49. public void Enable() => LifecycleEnable(Instance);
  50. public Task Disable() => LifecycleDisable(Instance);
  51. private void PrepareDelegates()
  52. { // TODO: use custom exception types or something
  53. PluginLoader.Load(Metadata);
  54. var type = Metadata.Assembly.GetType(Metadata.PluginType.FullName);
  55. CreatePlugin = MakeCreateFunc(type, Metadata.Name);
  56. LifecycleEnable = MakeLifecycleEnableFunc(type, Metadata.Name);
  57. LifecycleDisable = MakeLifecycleDisableFunc(type, Metadata.Name);
  58. }
  59. private static Func<PluginMetadata, object> MakeCreateFunc(Type type, string name)
  60. { // TODO: what do i want the visibiliy of Init methods to be?
  61. var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
  62. .Select(c => (c, attr: c.GetCustomAttribute<InitAttribute>()))
  63. .NonNull(t => t.attr)
  64. .OrderByDescending(t => t.c.GetParameters().Length)
  65. .Select(t => t.c).ToArray();
  66. if (ctors.Length > 1)
  67. Logger.loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters.");
  68. bool usingDefaultCtor = false;
  69. var ctor = ctors.FirstOrDefault();
  70. if (ctor == null)
  71. { // this is a normal case
  72. usingDefaultCtor = true;
  73. ctor = type.GetConstructor(Type.EmptyTypes);
  74. if (ctor == null)
  75. throw new InvalidOperationException($"{type.FullName} does not expose a public default constructor and has no constructors marked [Init]");
  76. }
  77. var initMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  78. .Select(m => (m, attr: m.GetCustomAttribute<InitAttribute>()))
  79. .NonNull(t => t.attr).Select(t => t.m).ToArray();
  80. // verify that they don't have lifecycle attributes on them
  81. foreach (var method in initMethods)
  82. {
  83. var attrs = method.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false);
  84. if (attrs.Length != 0)
  85. throw new InvalidOperationException($"Method {method} on {type.FullName} has both an [Init] attribute and a lifecycle attribute.");
  86. }
  87. // TODO: how do I make this work for .NET 3? FEC.LightExpression but hacked to work on .NET 3?
  88. var metaParam = Expression.Parameter(typeof(PluginMetadata), "meta");
  89. var objVar = ExpressionEx.Variable(type, "objVar");
  90. var persistVar = ExpressionEx.Variable(typeof(object), "persistVar");
  91. var createExpr = Expression.Lambda<Func<PluginMetadata, object>>(
  92. ExpressionEx.Block(new[] { objVar, persistVar },
  93. initMethods
  94. .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, persistVar, es => Expression.Call(objVar, m, es)))
  95. .Prepend(ExpressionEx.Assign(objVar,
  96. usingDefaultCtor
  97. ? Expression.New(ctor)
  98. : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, persistVar, es => Expression.New(ctor, es))))
  99. .Append(Expression.Convert(objVar, typeof(object)))),
  100. metaParam);
  101. // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler
  102. return createExpr.Compile();
  103. }
  104. // TODO: make enable and disable able to take a bool indicating which it is
  105. private static Action<object> MakeLifecycleEnableFunc(Type type, string name)
  106. {
  107. var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  108. .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false)))
  109. .Select(t => (t.m, attrs: t.attrs.Cast<IEdgeLifecycleAttribute>()))
  110. .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Enable))
  111. .Select(t => t.m).ToArray();
  112. if (enableMethods.Length == 0)
  113. {
  114. Logger.loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?");
  115. return o => { };
  116. }
  117. foreach (var m in enableMethods)
  118. {
  119. if (m.GetParameters().Length > 0)
  120. throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters.");
  121. if (m.ReturnType != typeof(void))
  122. Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored.");
  123. }
  124. var objParam = Expression.Parameter(typeof(object), "obj");
  125. var instVar = ExpressionEx.Variable(type, "inst");
  126. var createExpr = Expression.Lambda<Action<object>>(
  127. ExpressionEx.Block(new[] { instVar },
  128. enableMethods
  129. .Select(m => (Expression)Expression.Call(instVar, m))
  130. .Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))),
  131. objParam);
  132. return createExpr.Compile();
  133. }
  134. private static Func<object, Task> MakeLifecycleDisableFunc(Type type, string name)
  135. {
  136. var disableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  137. .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false)))
  138. .Select(t => (t.m, attrs: t.attrs.Cast<IEdgeLifecycleAttribute>()))
  139. .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Disable))
  140. .Select(t => t.m).ToArray();
  141. if (disableMethods.Length == 0)
  142. {
  143. Logger.loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?");
  144. return o => TaskEx.WhenAll();
  145. }
  146. var taskMethods = new List<MethodInfo>();
  147. var nonTaskMethods = new List<MethodInfo>();
  148. foreach (var m in disableMethods)
  149. {
  150. if (m.GetParameters().Length > 0)
  151. throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and has parameters.");
  152. if (m.ReturnType != typeof(void))
  153. {
  154. if (typeof(Task).IsAssignableFrom(m.ReturnType))
  155. {
  156. taskMethods.Add(m);
  157. continue;
  158. }
  159. else
  160. Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored.");
  161. }
  162. nonTaskMethods.Add(m);
  163. }
  164. Expression<Func<Task>> completedTaskDel = () => TaskEx.WhenAll();
  165. var getCompletedTask = completedTaskDel.Body;
  166. var taskWhenAll = typeof(TaskEx).GetMethod(nameof(TaskEx.WhenAll), new[] { typeof(Task[]) });
  167. var objParam = Expression.Parameter(typeof(object), "obj");
  168. var instVar = ExpressionEx.Variable(type, "inst");
  169. var createExpr = Expression.Lambda<Func<object, Task>>(
  170. ExpressionEx.Block(new[] { instVar },
  171. nonTaskMethods
  172. .Select(m => (Expression)Expression.Call(instVar, m))
  173. .Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))
  174. .Append(
  175. taskMethods.Count == 0
  176. ? getCompletedTask
  177. : Expression.Call(taskWhenAll,
  178. Expression.NewArrayInit(typeof(Task),
  179. taskMethods.Select(m =>
  180. (Expression)Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))),
  181. objParam);
  182. return createExpr.Compile();
  183. }
  184. }
  185. }