using IPA.Logging; using IPA.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Linq.Expressions; #if NET4 using Task = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task; using Expression = System.Linq.Expressions.Expression; using ExpressionEx = System.Linq.Expressions.Expression; #endif #if NET3 using System.Threading.Tasks; using Net3_Proxy; using Path = Net3_Proxy.Path; using File = Net3_Proxy.File; using Directory = Net3_Proxy.Directory; #endif namespace IPA.Loader { // NOTE: TaskEx.WhenAll() (Task.WhenAll() in .NET 4) returns CompletedTask if it has no arguments, which we need for .NET 3 internal class PluginExecutor { public enum Special { None, Self, Bare } public PluginMetadata Metadata { get; } public Special SpecialType { get; } public PluginExecutor(PluginMetadata meta, Special specialType = Special.None) { Metadata = meta; SpecialType = specialType; if (specialType != Special.None) { CreatePlugin = m => null; LifecycleEnable = o => { }; LifecycleDisable = o => TaskEx.WhenAll(); } else PrepareDelegates(); } public object Instance { get; private set; } = null; private Func CreatePlugin { get; set; } private Action LifecycleEnable { get; set; } // disable may be async (#24) private Func LifecycleDisable { get; set; } public void Create() { if (Instance != null) return; Instance = CreatePlugin(Metadata); } public void Enable() => LifecycleEnable(Instance); public Task Disable() => LifecycleDisable(Instance); private void PrepareDelegates() { // TODO: use custom exception types or something PluginLoader.Load(Metadata); var type = Metadata.Assembly.GetType(Metadata.PluginType.FullName); CreatePlugin = MakeCreateFunc(type, Metadata.Name); LifecycleEnable = MakeLifecycleEnableFunc(type, Metadata.Name); LifecycleDisable = MakeLifecycleDisableFunc(type, Metadata.Name); } private static Func MakeCreateFunc(Type type, string name) { // TODO: what do i want the visibiliy of Init methods to be? var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) .Select(c => (c, attr: c.GetCustomAttribute())) .NonNull(t => t.attr) .OrderByDescending(t => t.c.GetParameters().Length) .Select(t => t.c).ToArray(); if (ctors.Length > 1) Logger.loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters."); bool usingDefaultCtor = false; var ctor = ctors.FirstOrDefault(); if (ctor == null) { // this is a normal case usingDefaultCtor = true; ctor = type.GetConstructor(Type.EmptyTypes); if (ctor == null) throw new InvalidOperationException($"{type.FullName} does not expose a public default constructor and has no constructors marked [Init]"); } var initMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Select(m => (m, attr: m.GetCustomAttribute())) .NonNull(t => t.attr).Select(t => t.m).ToArray(); // verify that they don't have lifecycle attributes on them foreach (var method in initMethods) { var attrs = method.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false); if (attrs.Length != 0) throw new InvalidOperationException($"Method {method} on {type.FullName} has both an [Init] attribute and a lifecycle attribute."); } var metaParam = Expression.Parameter(typeof(PluginMetadata), "meta"); var objVar = ExpressionEx.Variable(type, "objVar"); var persistVar = ExpressionEx.Variable(typeof(object), "persistVar"); var createExpr = Expression.Lambda>( ExpressionEx.Block(new[] { objVar, persistVar }, initMethods .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, persistVar, es => Expression.Call(objVar, m, es))) .Prepend(ExpressionEx.Assign(objVar, usingDefaultCtor ? Expression.New(ctor) : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, persistVar, es => Expression.New(ctor, es)))) .Append(Expression.Convert(objVar, typeof(object)))), metaParam); // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler return createExpr.Compile(); } // TODO: make enable and disable able to take a bool indicating which it is private static Action MakeLifecycleEnableFunc(Type type, string name) { var noEnableDisable = type.GetCustomAttribute() is not null; var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) .Select(t => (t.m, attrs: t.attrs.Cast())) .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Enable)) .Select(t => t.m).ToArray(); if (enableMethods.Length == 0) { if (!noEnableDisable) Logger.loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?"); return o => { }; } foreach (var m in enableMethods) { if (m.GetParameters().Length > 0) throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters."); if (m.ReturnType != typeof(void)) Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored."); } var objParam = Expression.Parameter(typeof(object), "obj"); var instVar = ExpressionEx.Variable(type, "inst"); var createExpr = Expression.Lambda>( ExpressionEx.Block(new[] { instVar }, enableMethods .Select(m => (Expression)Expression.Call(instVar, m)) .Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))), objParam); return createExpr.Compile(); } private static Func MakeLifecycleDisableFunc(Type type, string name) { var noEnableDisable = type.GetCustomAttribute() is not null; var disableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) .Select(t => (t.m, attrs: t.attrs.Cast())) .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Disable)) .Select(t => t.m).ToArray(); if (disableMethods.Length == 0) { if (!noEnableDisable) Logger.loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?"); return o => TaskEx.WhenAll(); } var taskMethods = new List(); var nonTaskMethods = new List(); foreach (var m in disableMethods) { if (m.GetParameters().Length > 0) throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and has parameters."); if (m.ReturnType != typeof(void)) { if (typeof(Task).IsAssignableFrom(m.ReturnType)) { taskMethods.Add(m); continue; } else Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored."); } nonTaskMethods.Add(m); } Expression> completedTaskDel = () => TaskEx.WhenAll(); var getCompletedTask = completedTaskDel.Body; var taskWhenAll = typeof(TaskEx).GetMethod(nameof(TaskEx.WhenAll), new[] { typeof(Task[]) }); var objParam = Expression.Parameter(typeof(object), "obj"); var instVar = ExpressionEx.Variable(type, "inst"); var createExpr = Expression.Lambda>( ExpressionEx.Block(new[] { instVar }, nonTaskMethods .Select(m => (Expression)Expression.Call(instVar, m)) .Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type))) .Append( taskMethods.Count == 0 ? getCompletedTask : Expression.Call(taskWhenAll, Expression.NewArrayInit(typeof(Task), taskMethods.Select(m => (Expression)Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))), objParam); return createExpr.Compile(); } } }