using System; using System.Collections.Generic; 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 { /********* ** Consts *********/ private static readonly string TargetFieldName = "__Target"; private static readonly string GlueFieldName = "__Glue"; private static readonly MethodInfo CreateInstanceForProxyTypeNameMethod = typeof(InterfaceProxyGlue).GetMethod(nameof(InterfaceProxyGlue.CreateInstanceForProxyTypeName), new Type[] { typeof(string), typeof(object) }); /********* ** Fields *********/ /// The target class type. private readonly Type TargetType; /// The full name of the generated proxy type. private readonly string ProxyTypeName; /// The generated proxy type. private Type ProxyType; /********* ** Public methods *********/ /// Construct an instance. /// The target type. /// The type name to generate. public InterfaceProxyBuilder(Type targetType, string proxyTypeName) { // validate this.TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType)); this.ProxyTypeName = proxyTypeName ?? throw new ArgumentNullException(nameof(proxyTypeName)); } /// Creates and sets up the proxy type. /// The that requested to build a proxy. /// The CLR module in which to create proxy classes. /// The interface type to implement. /// The unique ID of the mod consuming the API. /// The unique ID of the mod providing the API. public void SetupProxyType(InterfaceProxyFactory factory, ModuleBuilder moduleBuilder, Type interfaceType, string sourceModID, string targetModID) { // define proxy type TypeBuilder proxyBuilder = moduleBuilder.DefineType(this.ProxyTypeName, TypeAttributes.Public | TypeAttributes.Class); proxyBuilder.AddInterfaceImplementation(interfaceType); // create fields to store target instance and proxy factory FieldBuilder targetField = proxyBuilder.DefineField(TargetFieldName, this.TargetType, FieldAttributes.Private); FieldBuilder glueField = proxyBuilder.DefineField(GlueFieldName, typeof(InterfaceProxyGlue), FieldAttributes.Private); // create constructor which accepts target instance + factory, and sets fields { ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { this.TargetType, typeof(InterfaceProxyGlue) }); 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.Ldarg_0); // this il.Emit(OpCodes.Ldarg_2); // load argument il.Emit(OpCodes.Stfld, glueField); // set field to loaded argument il.Emit(OpCodes.Ret); } var allTargetMethods = this.TargetType.GetMethods().ToList(); foreach (Type targetInterface in this.TargetType.GetInterfaces()) { foreach (MethodInfo targetMethod in targetInterface.GetMethods()) { if (!targetMethod.IsAbstract) allTargetMethods.Add(targetMethod); } } MatchingTypesResult AreTypesMatching(Type targetType, Type proxyType, MethodTypeMatchingPart part) { var typeA = part == MethodTypeMatchingPart.Parameter ? targetType : proxyType; var typeB = part == MethodTypeMatchingPart.Parameter ? proxyType : targetType; if (typeA.IsGenericMethodParameter != typeB.IsGenericMethodParameter) return MatchingTypesResult.False; // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) if (typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB)) return MatchingTypesResult.True; if (!proxyType.IsGenericMethodParameter && proxyType.GetNonRefType().IsInterface && proxyType.Assembly == interfaceType.Assembly) return MatchingTypesResult.IfProxied; return MatchingTypesResult.False; } // proxy methods foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) { var proxyMethodParameters = proxyMethod.GetParameters(); var proxyMethodGenericArguments = proxyMethod.GetGenericArguments(); foreach (MethodInfo targetMethod in allTargetMethods) { // checking if `targetMethod` matches `proxyMethod` if (targetMethod.Name != proxyMethod.Name) continue; if (targetMethod.GetGenericArguments().Length != proxyMethodGenericArguments.Length) continue; var positionsToProxy = new HashSet(); // null = return type; anything else = parameter position switch (AreTypesMatching(targetMethod.ReturnType, proxyMethod.ReturnType, MethodTypeMatchingPart.ReturnType)) { case MatchingTypesResult.False: continue; case MatchingTypesResult.True: break; case MatchingTypesResult.IfProxied: positionsToProxy.Add(null); break; } var mParameters = targetMethod.GetParameters(); if (mParameters.Length != proxyMethodParameters.Length) continue; for (int i = 0; i < mParameters.Length; i++) { switch (AreTypesMatching(mParameters[i].ParameterType, proxyMethodParameters[i].ParameterType, MethodTypeMatchingPart.Parameter)) { case MatchingTypesResult.False: goto targetMethodLoopContinue; case MatchingTypesResult.True: break; case MatchingTypesResult.IfProxied: if (proxyMethodParameters[i].IsOut) { positionsToProxy.Add(i); break; } else { goto targetMethodLoopContinue; } } } // method matched; proxying this.ProxyMethod(factory, proxyBuilder, proxyMethod, targetMethod, targetField, glueField, positionsToProxy, sourceModID, targetModID); goto proxyMethodLoopContinue; targetMethodLoopContinue:; } throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); proxyMethodLoopContinue:; } // save info this.ProxyType = proxyBuilder.CreateType(); } /// Create an instance of the proxy for a target instance. /// The target instance. /// The that requested to build a proxy. public object CreateInstance(object targetInstance, InterfaceProxyFactory factory) { ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType, typeof(InterfaceProxyGlue) }); 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, new InterfaceProxyGlue(factory) }); } /********* ** Private methods *********/ /// Define a method which proxies access to a method on the target. /// The that requested to build a proxy. /// The proxy type being generated. /// The proxy method. /// The target method. /// The proxy field containing the API instance. /// The proxy field containing an . /// Parameter type positions (or null for the return type) for which types should also be proxied. /// The unique ID of the mod consuming the API. /// The unique ID of the mod providing the API. private void ProxyMethod(InterfaceProxyFactory factory, TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField, FieldBuilder glueField, ISet positionsToProxy, string sourceModID, string targetModID) { MethodBuilder methodBuilder = proxyBuilder.DefineMethod(proxy.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); // set up generic arguments Type[] proxyGenericArguments = proxy.GetGenericArguments(); string[] genericArgNames = proxyGenericArguments.Select(a => a.Name).ToArray(); GenericTypeParameterBuilder[] genericTypeParameterBuilders = proxyGenericArguments.Length == 0 ? null : methodBuilder.DefineGenericParameters(genericArgNames); for (int i = 0; i < proxyGenericArguments.Length; i++) genericTypeParameterBuilders[i].SetGenericParameterAttributes(proxyGenericArguments[i].GenericParameterAttributes); // set up return type Type returnType = proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType; methodBuilder.SetReturnType(returnType); // set up parameters var targetParameters = target.GetParameters(); Type[] argTypes = proxy.GetParameters() .Select(a => a.ParameterType) .Select(t => t.IsGenericMethodParameter ? genericTypeParameterBuilders[t.GenericParameterPosition] : t) .ToArray(); // proxy additional types string returnValueProxyTypeName = null; string[] parameterProxyTypeNames = new string[argTypes.Length]; if (positionsToProxy.Count > 0) { foreach (int? position in positionsToProxy) { // we don't check for generics here, because earlier code does and generic positions won't end up here if (position == null) // it's the return type { var builder = factory.ObtainBuilder(target.ReturnType, proxy.ReturnType, sourceModID, targetModID); returnType = proxy.ReturnType; returnValueProxyTypeName = builder.ProxyTypeName; } else // it's one of the parameters { bool isByRef = argTypes[position.Value].IsByRef; var targetType = targetParameters[position.Value].ParameterType.GetNonRefType(); var argType = argTypes[position.Value].GetNonRefType(); var builder = factory.ObtainBuilder(targetType, argType, sourceModID, targetModID); if (isByRef) argType = argType.MakeByRefType(); argTypes[position.Value] = argType; parameterProxyTypeNames[position.Value] = builder.ProxyTypeName; } } methodBuilder.SetReturnType(returnType); } methodBuilder.SetParameters(argTypes); for (int i = 0; i < argTypes.Length; i++) methodBuilder.DefineParameter(i, targetParameters[i].Attributes, targetParameters[i].Name); // create method body { ILGenerator il = methodBuilder.GetILGenerator(); LocalBuilder[] outInputLocals = new LocalBuilder[argTypes.Length]; LocalBuilder[] outOutputLocals = new LocalBuilder[argTypes.Length]; // calling the proxied method LocalBuilder resultInputLocal = target.ReturnType == typeof(void) ? null : il.DeclareLocal(target.ReturnType); LocalBuilder resultOutputLocal = returnType == typeof(void) ? null : il.DeclareLocal(returnType); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, instanceField); for (int i = 0; i < argTypes.Length; i++) { if (parameterProxyTypeNames[i] == null) { il.Emit(OpCodes.Ldarg, i + 1); } else { // previous code already checks if the parameters are specifically `out` outInputLocals[i] = il.DeclareLocal(targetParameters[i].ParameterType.GetNonRefType()); outOutputLocals[i] = il.DeclareLocal(argTypes[i].GetNonRefType()); il.Emit(OpCodes.Ldloca, outInputLocals[i]); } } il.Emit(OpCodes.Callvirt, target); if (target.ReturnType != typeof(void)) il.Emit(OpCodes.Stloc, resultInputLocal); void ProxyIfNeededAndStore(LocalBuilder inputLocal, LocalBuilder outputLocal, string proxyTypeName) { if (proxyTypeName == null) { il.Emit(OpCodes.Ldloc, inputLocal); il.Emit(OpCodes.Stloc, outputLocal); return; } var isNullLabel = il.DefineLabel(); il.Emit(OpCodes.Ldloc, inputLocal); il.Emit(OpCodes.Brfalse, isNullLabel); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, glueField); il.Emit(OpCodes.Ldstr, proxyTypeName); il.Emit(OpCodes.Ldloc, inputLocal); il.Emit(OpCodes.Call, CreateInstanceForProxyTypeNameMethod); il.Emit(OpCodes.Castclass, outputLocal.LocalType); il.Emit(OpCodes.Stloc, outputLocal); il.MarkLabel(isNullLabel); } // proxying `out` parameters for (int i = 0; i < argTypes.Length; i++) { if (parameterProxyTypeNames[i] == null) continue; // previous code already checks if the parameters are specifically `out` ProxyIfNeededAndStore(outInputLocals[i], outOutputLocals[i], parameterProxyTypeNames[i]); il.Emit(OpCodes.Ldarg, i + 1); il.Emit(OpCodes.Ldloc, outOutputLocals[i]); il.Emit(OpCodes.Stind_Ref); } // proxying return value if (target.ReturnType != typeof(void)) ProxyIfNeededAndStore(resultInputLocal, resultOutputLocal, returnValueProxyTypeName); // return result if (target.ReturnType != typeof(void)) il.Emit(OpCodes.Ldloc, resultOutputLocal); il.Emit(OpCodes.Ret); } } /// The part of a method that is being matched. private enum MethodTypeMatchingPart { ReturnType, Parameter } /// The result of matching a target and a proxy type. private enum MatchingTypesResult { False, IfProxied, True } } internal static class TypeExtensions { internal static Type GetNonRefType(this Type type) { return type.IsByRef ? type.GetElementType() : type; } } }