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.

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