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.

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