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 { /********* ** Properties *********/ /// 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); } // proxy methods foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) { var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray()); 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); } } } }