using System; using System.Linq; using System.Reflection; using System.Reflection.Emit; namespace StardewModdingAPI.Framework.Reflection { /// Generates a proxy class to access a mod API through an arbitrary interface. internal class InterfaceProxyBuilder { /********* ** Fields *********/ /// The target class type. private readonly Type TargetType; /// The generated proxy type. private readonly Type ProxyType; /********* ** Public methods *********/ /// Construct an instance. /// The type name to generate. /// The CLR module in which to create proxy classes. /// The interface type to implement. /// The target type. public InterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType) { // validate if (name == null) throw new ArgumentNullException(nameof(name)); if (targetType == null) throw new ArgumentNullException(nameof(targetType)); // define proxy type TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class); proxyBuilder.AddInterfaceImplementation(interfaceType); // create field to store target instance FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); // create constructor which accepts target instance and sets field { ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); ILGenerator il = constructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // this // ReSharper disable once AssignNullToNotNullAttribute -- never null il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // call base constructor il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_1); // load argument il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument il.Emit(OpCodes.Ret); } var allTargetMethods = targetType.GetMethods().ToList(); foreach (Type targetInterface in targetType.GetInterfaces()) { foreach (MethodInfo targetMethod in targetInterface.GetMethods()) { if (!targetMethod.IsAbstract) allTargetMethods.Add(targetMethod); } } // proxy methods foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) { var proxyMethodParameters = proxyMethod.GetParameters(); var targetMethod = allTargetMethods.Where(m => { if (m.Name != proxyMethod.Name) return false; if (m.ReturnType != proxyMethod.ReturnType) return false; var mParameters = m.GetParameters(); if (m.GetParameters().Length != proxyMethodParameters.Length) return false; for (int i = 0; i < mParameters.Length; i++) { // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) // TODO: test if this actually works if (!mParameters[i].ParameterType.IsAssignableFrom(proxyMethodParameters[i].ParameterType)) return false; } return true; }).FirstOrDefault(); if (targetMethod == null) throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); this.ProxyMethod(proxyBuilder, targetMethod, targetField); } // save info this.TargetType = targetType; this.ProxyType = proxyBuilder.CreateType(); } /// Create an instance of the proxy for a target instance. /// The target instance. public object CreateInstance(object targetInstance) { ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); if (constructor == null) throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen return constructor.Invoke(new[] { targetInstance }); } /********* ** Private methods *********/ /// Define a method which proxies access to a method on the target. /// The proxy type being generated. /// The target method. /// The proxy field containing the API instance. private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField) { Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray(); // create method MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); methodBuilder.SetParameters(argTypes); methodBuilder.SetReturnType(target.ReturnType); // create method body { ILGenerator il = methodBuilder.GetILGenerator(); // load target instance il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, instanceField); // invoke target method on instance for (int i = 0; i < argTypes.Length; i++) il.Emit(OpCodes.Ldarg, i + 1); il.Emit(OpCodes.Call, target); // return result il.Emit(OpCodes.Ret); } } } }