diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index 96355e3d..d16550ed 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -135,6 +135,7 @@ namespace IPA.Loader public PluginExecutor(PluginMetadata meta) { Metadata = meta; + PrepareDelegates(); } @@ -159,51 +160,142 @@ namespace IPA.Loader Load(Metadata); var type = Metadata.Assembly.GetType(Metadata.PluginType.FullName); - { // 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 {Metadata.Name} has multiple [Init] constructors. Picking the one with the most parameters."); - - bool usingDefaultCtor = false; - var ctor = ctors.FirstOrDefault(); + 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) - { // 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]"); - } + 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."); + } + + // TODO: how do I make this work for .NET 3? FEC.LightExpression but hacked to work on .NET 3? + var metaParam = Expression.Parameter(typeof(PluginMetadata)); + var objVar = Expression.Variable(type); + var createExpr = Expression.Lambda>( + Expression.Block( + initMethods + .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, es => Expression.Call(objVar, m, es))) + .Prepend(Expression.Assign(objVar, + usingDefaultCtor + ? Expression.New(ctor) + : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, 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(); + } + private static Action MakeLifecycleEnableFunc(Type type, string name) + { + 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) + { + 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)); + var instVar = Expression.Variable(type); + var createExpr = Expression.Lambda>( + Expression.Block( + enableMethods + .Select(m => Expression.Call(instVar, m)) + .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type)))), + objParam); + return createExpr.Compile(); + } + private static Func MakeLifecycleDisableFunc(Type type, string name) + { + 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) + { + Logger.loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?"); + return o => Task.CompletedTask; + } - 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 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)) { - 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."); + 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."); } - var metaParam = Expression.Parameter(typeof(PluginMetadata)); - var objVar = Expression.Variable(type); - var createExpr = Expression.Lambda>( - Expression.Block( - initMethods - .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, es => Expression.Call(objVar, m, es))) - .Prepend(Expression.Assign(objVar, - usingDefaultCtor - ? Expression.New(ctor) - : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, es => Expression.New(ctor, es)))) - .Append(objVar)), - metaParam); - // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler - CreatePlugin = createExpr.Compile(); + nonTaskMethods.Add(m); } + + Expression> completedTaskDel = () => Task.CompletedTask; + var getCompletedTask = completedTaskDel.Body; + var taskWhenAll = typeof(Task).GetMethod(nameof(Task.WhenAll), BindingFlags.Public | BindingFlags.Static); + + var objParam = Expression.Parameter(typeof(object)); + var instVar = Expression.Variable(type); + var createExpr = Expression.Lambda>( + Expression.Block( + nonTaskMethods + .Select(m => Expression.Call(instVar, m)) + .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type))) + .Append( + taskMethods.Count == 0 + ? getCompletedTask + : Expression.Call(taskWhenAll, + Expression.NewArrayInit(typeof(Task), + taskMethods.Select(m => + Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))), + objParam); + return createExpr.Compile(); } }