using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; namespace StardewModdingAPI.Framework.Reflection { /// Generates proxy classes to access mod APIs through an arbitrary interface. internal class InterfaceProxyBuilder { /********* ** Properties *********/ /// The CLR module in which to create proxy classes. private readonly ModuleBuilder ModuleBuilder; /// The generated proxy types. private readonly IDictionary GeneratedTypes = new Dictionary(); /********* ** Public methods *********/ /// Construct an instance. public InterfaceProxyBuilder() { AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); } /// Create an API proxy. /// The interface through which to access the API. /// The API instance to access. /// The unique ID of the mod consuming the API. /// The unique ID of the mod providing the API. public TInterface CreateProxy(object instance, string sourceModID, string targetModID) where TInterface : class { // validate if (instance == null) throw new InvalidOperationException("Can't proxy access to a null API."); if (!typeof(TInterface).IsInterface) throw new InvalidOperationException("The proxy type must be an interface, not a class."); // get proxy type Type targetType = instance.GetType(); string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; if (!this.GeneratedTypes.TryGetValue(proxyTypeName, out Type type)) { type = this.CreateProxyType(proxyTypeName, typeof(TInterface), targetType); this.GeneratedTypes[proxyTypeName] = type; } // create instance ConstructorInfo constructor = type.GetConstructor(new[] { targetType }); if (constructor == null) throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen return (TInterface)constructor.Invoke(new[] { instance }); } /********* ** Private methods *********/ /// Define a class which proxies access to a target type through an interface. /// The name of the proxy type to generate. /// The interface type through which to access the target. /// The target type to access. private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType) { // define proxy type TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class); proxyBuilder.AddInterfaceImplementation(interfaceType); // create field to store target instance FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); // create constructor which accepts target instance { 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, field); // 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, field); } // create type return proxyBuilder.CreateType(); } /// 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); } } } }