From 0ff82c38e7a5b630256d2cd23a63ac1088d13e39 Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 20:02:13 +0100 Subject: allow default interface method implementations in API proxies --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 70ef81f8..164cac0b 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -55,10 +55,39 @@ namespace StardewModdingAPI.Framework.Reflection 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 targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray()); + 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."); -- cgit From 3135925982b01d8f316adc171c7c1235ea41c1d3 Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 21:54:53 +0100 Subject: allow generic methods and any assignable types in API proxies --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 53 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 164cac0b..35faa852 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -65,15 +65,30 @@ namespace StardewModdingAPI.Framework.Reflection } } + bool 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 false; + // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) + return typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB); + } + // proxy methods foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) { var proxyMethodParameters = proxyMethod.GetParameters(); + var proxyMethodGenericArguments = proxyMethod.GetGenericArguments(); var targetMethod = allTargetMethods.Where(m => { if (m.Name != proxyMethod.Name) return false; - if (m.ReturnType != proxyMethod.ReturnType) + + if (m.GetGenericArguments().Length != proxyMethodGenericArguments.Length) + return false; + if (!AreTypesMatching(m.ReturnType, proxyMethod.ReturnType, MethodTypeMatchingPart.ReturnType)) return false; var mParameters = m.GetParameters(); @@ -81,9 +96,7 @@ namespace StardewModdingAPI.Framework.Reflection 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)) + if (!AreTypesMatching(mParameters[i].ParameterType, proxyMethodParameters[i].ParameterType, MethodTypeMatchingPart.Parameter)) return false; } return true; @@ -91,7 +104,7 @@ namespace StardewModdingAPI.Framework.Reflection 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); + this.ProxyMethod(proxyBuilder, proxyMethod, targetMethod, targetField); } // save info @@ -115,16 +128,30 @@ namespace StardewModdingAPI.Framework.Reflection *********/ /// Define a method which proxies access to a method on the target. /// The proxy type being generated. + /// The proxy method. /// The target method. /// The proxy field containing the API instance. - private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField) + private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo proxy, 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); + + // 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 + // TODO: keep if it's decided to use isAssignableFrom + methodBuilder.SetReturnType(proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType); + + // set up parameters + Type[] argTypes = proxy.GetParameters() + .Select(a => a.ParameterType) + .Select(t => t.IsGenericMethodParameter ? genericTypeParameterBuilders[t.GenericParameterPosition] : t) + .ToArray(); methodBuilder.SetParameters(argTypes); - methodBuilder.SetReturnType(target.ReturnType); // create method body { @@ -143,5 +170,11 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ret); } } + + /// The part of a method that is being matched. + private enum MethodTypeMatchingPart + { + ReturnType, Parameter + } } } -- cgit From 5b5304403b79ce09bd06d1c4b52b67427069b772 Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 22:19:16 +0100 Subject: oops old code --- src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 35faa852..4c154679 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -143,7 +143,6 @@ namespace StardewModdingAPI.Framework.Reflection genericTypeParameterBuilders[i].SetGenericParameterAttributes(proxyGenericArguments[i].GenericParameterAttributes); // set up return type - // TODO: keep if it's decided to use isAssignableFrom methodBuilder.SetReturnType(proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType); // set up parameters -- cgit From 5a92b0cd357776eebb88e001384f9ca1ccdb7d5c Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 22:36:34 +0100 Subject: uses `proxy.Name` instead of `target.Name` (which makes more sense in this context) --- src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 4c154679..5ae96dff 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -133,7 +133,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The proxy field containing the API instance. private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField) { - MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); + MethodBuilder methodBuilder = proxyBuilder.DefineMethod(proxy.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); // set up generic arguments Type[] proxyGenericArguments = proxy.GetGenericArguments(); -- cgit From 43ad219b75740ef71ad9bad496b00c076182619b Mon Sep 17 00:00:00 2001 From: Shockah Date: Wed, 9 Feb 2022 20:07:01 +0100 Subject: support proxying return values in API proxies --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 183 ++++++++++++++++----- .../Framework/Reflection/InterfaceProxyFactory.cs | 30 +++- .../Framework/Reflection/InterfaceProxyGlue.cs | 18 ++ 3 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 5ae96dff..d8b066bd 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -1,13 +1,22 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using HarmonyLib; 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 *********/ @@ -22,11 +31,14 @@ namespace StardewModdingAPI.Framework.Reflection ** Public methods *********/ /// Construct an instance. + /// The that requested to build a proxy. /// 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) + /// The unique ID of the mod consuming the API. + /// The unique ID of the mod providing the API. + public InterfaceProxyBuilder(InterfaceProxyFactory factory, string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType, string sourceModID, string targetModID) { // validate if (name == null) @@ -38,12 +50,13 @@ namespace StardewModdingAPI.Framework.Reflection 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 fields to store target instance and proxy factory + FieldBuilder targetField = proxyBuilder.DefineField(TargetFieldName, targetType, FieldAttributes.Private); + FieldBuilder glueField = proxyBuilder.DefineField(GlueFieldName, typeof(InterfaceProxyGlue), FieldAttributes.Private); - // create constructor which accepts target instance and sets field + // create constructor which accepts target instance + factory, and sets fields { - ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); + ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType, typeof(InterfaceProxyGlue) }); ILGenerator il = constructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // this @@ -52,6 +65,9 @@ namespace StardewModdingAPI.Framework.Reflection 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); } @@ -65,15 +81,20 @@ namespace StardewModdingAPI.Framework.Reflection } } - bool AreTypesMatching(Type targetType, Type proxyType, MethodTypeMatchingPart part) + 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 false; + return MatchingTypesResult.False; // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) - return typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB); + if (typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB)) + return MatchingTypesResult.True; + + if (!proxyType.IsGenericMethodParameter && proxyType.IsInterface && proxyType.Assembly == interfaceType.Assembly) + return MatchingTypesResult.IfProxied; + return MatchingTypesResult.False; } // proxy methods @@ -81,30 +102,61 @@ namespace StardewModdingAPI.Framework.Reflection { var proxyMethodParameters = proxyMethod.GetParameters(); var proxyMethodGenericArguments = proxyMethod.GetGenericArguments(); - var targetMethod = allTargetMethods.Where(m => + + foreach (MethodInfo targetMethod in allTargetMethods) { - if (m.Name != proxyMethod.Name) - return false; + // checking if `targetMethod` matches `proxyMethod` - if (m.GetGenericArguments().Length != proxyMethodGenericArguments.Length) - return false; - if (!AreTypesMatching(m.ReturnType, proxyMethod.ReturnType, MethodTypeMatchingPart.ReturnType)) - return false; + 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 = m.GetParameters(); - if (m.GetParameters().Length != proxyMethodParameters.Length) - return false; + var mParameters = targetMethod.GetParameters(); + if (mParameters.Length != proxyMethodParameters.Length) + continue; for (int i = 0; i < mParameters.Length; i++) { - if (!AreTypesMatching(mParameters[i].ParameterType, proxyMethodParameters[i].ParameterType, MethodTypeMatchingPart.Parameter)) - return false; + 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; + } + } } - 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, proxyMethod, targetMethod, targetField); + // 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 @@ -114,12 +166,13 @@ namespace StardewModdingAPI.Framework.Reflection /// Create an instance of the proxy for a target instance. /// The target instance. - public object CreateInstance(object targetInstance) + /// The that requested to build a proxy. + public object CreateInstance(object targetInstance, InterfaceProxyFactory factory) { - ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); + 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 }); + return constructor.Invoke(new[] { targetInstance, new InterfaceProxyGlue(factory) }); } @@ -127,11 +180,16 @@ namespace StardewModdingAPI.Framework.Reflection ** 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. - private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField) + /// 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); @@ -143,7 +201,8 @@ namespace StardewModdingAPI.Framework.Reflection genericTypeParameterBuilders[i].SetGenericParameterAttributes(proxyGenericArguments[i].GenericParameterAttributes); // set up return type - methodBuilder.SetReturnType(proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType); + Type returnType = proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType; + methodBuilder.SetReturnType(returnType); // set up parameters Type[] argTypes = proxy.GetParameters() @@ -152,18 +211,62 @@ namespace StardewModdingAPI.Framework.Reflection .ToArray(); methodBuilder.SetParameters(argTypes); + // proxy additional types + string returnValueProxyTypeName = null; + string[] parameterProxyTypeNames = new string[argTypes.Length]; + if (positionsToProxy.Count > 0) + { + var targetParameters = target.GetParameters(); + 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.ProxyType.FullName; + } + else // it's one of the parameters + { + var builder = factory.ObtainBuilder(targetParameters[position.Value].ParameterType, argTypes[position.Value], sourceModID, targetModID); + argTypes[position.Value] = proxy.ReturnType; + parameterProxyTypeNames[position.Value] = builder.ProxyType.FullName; + } + } + + methodBuilder.SetReturnType(returnType); + methodBuilder.SetParameters(argTypes); + } + // create method body { ILGenerator il = methodBuilder.GetILGenerator(); - // load target instance - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, instanceField); + void EmitCallInstance() + { + // 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.Callvirt, target); + } - // invoke target method on instance - for (int i = 0; i < argTypes.Length; i++) - il.Emit(OpCodes.Ldarg, i + 1); - il.Emit(OpCodes.Call, target); + if (returnValueProxyTypeName == null) + { + EmitCallInstance(); + } + else + { + // this.Glue.CreateInstanceForProxyTypeName(proxyTypeName, this.Instance.Call(args)) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, glueField); + il.Emit(OpCodes.Ldstr, returnValueProxyTypeName); + EmitCallInstance(); + il.Emit(OpCodes.Call, CreateInstanceForProxyTypeNameMethod); + } // return result il.Emit(OpCodes.Ret); @@ -175,5 +278,11 @@ namespace StardewModdingAPI.Framework.Reflection { ReturnType, Parameter } + + /// The result of matching a target and a proxy type. + private enum MatchingTypesResult + { + False, IfProxied, True + } } } diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs index 5acba569..8ce187bf 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -35,26 +35,40 @@ namespace StardewModdingAPI.Framework.Reflection /// 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."); + + // create instance + InterfaceProxyBuilder builder = this.ObtainBuilder(instance.GetType(), typeof(TInterface), sourceModID, targetModID); + return (TInterface)builder.CreateInstance(instance, this); + } + + internal InterfaceProxyBuilder ObtainBuilder(Type targetType, Type interfaceType, string sourceModID, string targetModID) { lock (this.Builders) { // validate - if (instance == null) - throw new InvalidOperationException("Can't proxy access to a null API."); - if (!typeof(TInterface).IsInterface) + if (!interfaceType.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}>"; + string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{interfaceType.FullName}>_To<{targetModID}_{targetType.FullName}>"; if (!this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder)) { - builder = new InterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType); + builder = new InterfaceProxyBuilder(this, proxyTypeName, this.ModuleBuilder, interfaceType, targetType, sourceModID, targetModID); this.Builders[proxyTypeName] = builder; } + return builder; + } + } - // create instance - return (TInterface)builder.CreateInstance(instance); + internal InterfaceProxyBuilder GetBuilderByProxyTypeName(string proxyTypeName) + { + lock (this.Builders) + { + return this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder) ? builder : null; } } } diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs new file mode 100644 index 00000000..4e027252 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.Reflection +{ + public sealed class InterfaceProxyGlue + { + private readonly InterfaceProxyFactory Factory; + + internal InterfaceProxyGlue(InterfaceProxyFactory factory) + { + this.Factory = factory; + } + + public object CreateInstanceForProxyTypeName(string proxyTypeName, object toProxy) + { + var builder = this.Factory.GetBuilderByProxyTypeName(proxyTypeName); + return builder.CreateInstance(toProxy, this.Factory); + } + } +} -- cgit From ee78ab3c3710639ec7eecb3d2edc7f26ff998407 Mon Sep 17 00:00:00 2001 From: Shockah Date: Wed, 9 Feb 2022 20:38:14 +0100 Subject: fix stack overflow for proxied types referencing each other --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 41 ++++++++++++---------- .../Framework/Reflection/InterfaceProxyFactory.cs | 11 +++++- 2 files changed, 33 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index d8b066bd..99aea75c 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -23,40 +23,46 @@ namespace StardewModdingAPI.Framework.Reflection /// 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 readonly Type ProxyType; + 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 type name to generate. /// The CLR module in which to create proxy classes. /// The interface type to implement. - /// The target type. /// The unique ID of the mod consuming the API. /// The unique ID of the mod providing the API. - public InterfaceProxyBuilder(InterfaceProxyFactory factory, string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType, string sourceModID, string targetModID) + public void SetupProxyType(InterfaceProxyFactory factory, ModuleBuilder moduleBuilder, Type interfaceType, string sourceModID, string targetModID) { - // 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); + 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, targetType, FieldAttributes.Private); + 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[] { targetType, typeof(InterfaceProxyGlue) }); + 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 @@ -71,8 +77,8 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ret); } - var allTargetMethods = targetType.GetMethods().ToList(); - foreach (Type targetInterface in targetType.GetInterfaces()) + var allTargetMethods = this.TargetType.GetMethods().ToList(); + foreach (Type targetInterface in this.TargetType.GetInterfaces()) { foreach (MethodInfo targetMethod in targetInterface.GetMethods()) { @@ -160,7 +166,6 @@ namespace StardewModdingAPI.Framework.Reflection } // save info - this.TargetType = targetType; this.ProxyType = proxyBuilder.CreateType(); } @@ -224,13 +229,13 @@ namespace StardewModdingAPI.Framework.Reflection { var builder = factory.ObtainBuilder(target.ReturnType, proxy.ReturnType, sourceModID, targetModID); returnType = proxy.ReturnType; - returnValueProxyTypeName = builder.ProxyType.FullName; + returnValueProxyTypeName = builder.ProxyTypeName; } else // it's one of the parameters { var builder = factory.ObtainBuilder(targetParameters[position.Value].ParameterType, argTypes[position.Value], sourceModID, targetModID); argTypes[position.Value] = proxy.ReturnType; - parameterProxyTypeNames[position.Value] = builder.ProxyType.FullName; + parameterProxyTypeNames[position.Value] = builder.ProxyTypeName; } } diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs index 8ce187bf..72b4254c 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -57,8 +57,17 @@ namespace StardewModdingAPI.Framework.Reflection string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{interfaceType.FullName}>_To<{targetModID}_{targetType.FullName}>"; if (!this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder)) { - builder = new InterfaceProxyBuilder(this, proxyTypeName, this.ModuleBuilder, interfaceType, targetType, sourceModID, targetModID); + builder = new InterfaceProxyBuilder(targetType, proxyTypeName); this.Builders[proxyTypeName] = builder; + try + { + builder.SetupProxyType(this, this.ModuleBuilder, interfaceType, sourceModID, targetModID); + } + catch + { + this.Builders.Remove(proxyTypeName); + throw; + } } return builder; } -- cgit From 688fccc0246be6756a07933696a235dca8f1a395 Mon Sep 17 00:00:00 2001 From: Shockah Date: Wed, 9 Feb 2022 20:40:54 +0100 Subject: add missing documentation --- src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs index 4e027252..8d0d74a7 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs @@ -1,5 +1,6 @@ namespace StardewModdingAPI.Framework.Reflection { + /// Provides an interface for proxied types to create other proxied types. public sealed class InterfaceProxyGlue { private readonly InterfaceProxyFactory Factory; @@ -9,6 +10,9 @@ namespace StardewModdingAPI.Framework.Reflection this.Factory = factory; } + /// Creates a new proxied instance by its type name. + /// The full name of the proxy type. + /// The target instance to proxy. public object CreateInstanceForProxyTypeName(string proxyTypeName, object toProxy) { var builder = this.Factory.GetBuilderByProxyTypeName(proxyTypeName); -- cgit From 354527bb810d06f69f2a8c45d1a23f294d228caf Mon Sep 17 00:00:00 2001 From: Shockah Date: Wed, 9 Feb 2022 21:02:41 +0100 Subject: stop proxying nulls --- src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 99aea75c..1b6bf1d6 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -265,12 +265,24 @@ namespace StardewModdingAPI.Framework.Reflection } else { - // this.Glue.CreateInstanceForProxyTypeName(proxyTypeName, this.Instance.Call(args)) + var resultLocal = il.DeclareLocal(typeof(object)); // we store both unmodified and modified in here + EmitCallInstance(); + il.Emit(OpCodes.Stloc, resultLocal); + + // if (unmodifiedResultLocal == null) jump + var isNullLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Brfalse, isNullLabel); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, glueField); il.Emit(OpCodes.Ldstr, returnValueProxyTypeName); - EmitCallInstance(); + il.Emit(OpCodes.Ldloc, resultLocal); il.Emit(OpCodes.Call, CreateInstanceForProxyTypeNameMethod); + il.Emit(OpCodes.Stloc, resultLocal); + + il.MarkLabel(isNullLabel); + il.Emit(OpCodes.Ldloc, resultLocal); } // return result -- cgit From d9599a3a0af500438fa71addb1a25d4608aefda5 Mon Sep 17 00:00:00 2001 From: Shockah Date: Wed, 9 Feb 2022 21:10:20 +0100 Subject: simplifies proxy method IL a bit --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 32 +++++++--------------- 1 file changed, 10 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 1b6bf1d6..5e0dd838 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -247,28 +247,16 @@ namespace StardewModdingAPI.Framework.Reflection { ILGenerator il = methodBuilder.GetILGenerator(); - void EmitCallInstance() + var resultLocal = il.DeclareLocal(typeof(object)); // we store both unmodified and modified in here, hence `object` + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, instanceField); + for (int i = 0; i < argTypes.Length; i++) + il.Emit(OpCodes.Ldarg, i + 1); + il.Emit(OpCodes.Callvirt, target); + il.Emit(OpCodes.Stloc, resultLocal); + + if (returnValueProxyTypeName != null) { - // 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.Callvirt, target); - } - - if (returnValueProxyTypeName == null) - { - EmitCallInstance(); - } - else - { - var resultLocal = il.DeclareLocal(typeof(object)); // we store both unmodified and modified in here - EmitCallInstance(); - il.Emit(OpCodes.Stloc, resultLocal); - // if (unmodifiedResultLocal == null) jump var isNullLabel = il.DefineLabel(); il.Emit(OpCodes.Ldloc, resultLocal); @@ -282,10 +270,10 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Stloc, resultLocal); il.MarkLabel(isNullLabel); - il.Emit(OpCodes.Ldloc, resultLocal); } // return result + il.Emit(OpCodes.Ldloc, resultLocal); il.Emit(OpCodes.Ret); } } -- cgit From f920ed59d6234df9d33b0d72ea0398a3fa8e9b8b Mon Sep 17 00:00:00 2001 From: Shockah Date: Wed, 9 Feb 2022 23:26:26 +0100 Subject: add WIP proxying of methods with `out` parameters --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 66 ++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 5e0dd838..b1b3c451 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -98,7 +98,7 @@ namespace StardewModdingAPI.Framework.Reflection if (typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB)) return MatchingTypesResult.True; - if (!proxyType.IsGenericMethodParameter && proxyType.IsInterface && proxyType.Assembly == interfaceType.Assembly) + if (!proxyType.IsGenericMethodParameter && proxyType.GetNonRefType().IsInterface && proxyType.Assembly == interfaceType.Assembly) return MatchingTypesResult.IfProxied; return MatchingTypesResult.False; } @@ -233,8 +233,15 @@ namespace StardewModdingAPI.Framework.Reflection } else // it's one of the parameters { - var builder = factory.ObtainBuilder(targetParameters[position.Value].ParameterType, argTypes[position.Value], sourceModID, targetModID); - argTypes[position.Value] = proxy.ReturnType; + 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; } } @@ -246,32 +253,63 @@ namespace StardewModdingAPI.Framework.Reflection // create method body { ILGenerator il = methodBuilder.GetILGenerator(); + LocalBuilder[] outLocals = new LocalBuilder[argTypes.Length]; + // calling the proxied method var resultLocal = il.DeclareLocal(typeof(object)); // we store both unmodified and modified in here, hence `object` il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, instanceField); for (int i = 0; i < argTypes.Length; i++) - il.Emit(OpCodes.Ldarg, i + 1); + { + if (parameterProxyTypeNames[i] == null) + { + il.Emit(OpCodes.Ldarg, i + 1); + } + else + { + // previous code already checks if the parameters are specifically `out` + outLocals[i] = il.DeclareLocal(typeof(object)); // we store both unmodified and modified in here, hence `object` + il.Emit(OpCodes.Ldloca_S, outLocals[i]); + } + } il.Emit(OpCodes.Callvirt, target); il.Emit(OpCodes.Stloc, resultLocal); - if (returnValueProxyTypeName != null) + void ProxyNonNullIfNeeded(LocalBuilder local, string proxyTypeName) { - // if (unmodifiedResultLocal == null) jump + if (proxyTypeName == null) + return; + var isNullLabel = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldloc, local); il.Emit(OpCodes.Brfalse, isNullLabel); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, glueField); - il.Emit(OpCodes.Ldstr, returnValueProxyTypeName); - il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, proxyTypeName); + il.Emit(OpCodes.Ldloc, local); il.Emit(OpCodes.Call, CreateInstanceForProxyTypeNameMethod); - il.Emit(OpCodes.Stloc, resultLocal); + il.Emit(OpCodes.Stloc, local); 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` + + ProxyNonNullIfNeeded(outLocals[i], parameterProxyTypeNames[i]); + il.Emit(OpCodes.Ldarg, i + 1); + il.Emit(OpCodes.Ldloc, outLocals[i]); + il.Emit(OpCodes.Stind_Ref); + } + + // proxying return value + ProxyNonNullIfNeeded(resultLocal, returnValueProxyTypeName); + // return result il.Emit(OpCodes.Ldloc, resultLocal); il.Emit(OpCodes.Ret); @@ -290,4 +328,12 @@ namespace StardewModdingAPI.Framework.Reflection False, IfProxied, True } } + + internal static class TypeExtensions + { + internal static Type GetNonRefType(this Type type) + { + return type.IsByRef ? type.GetElementType() : type; + } + } } -- cgit From 55723f91d270fa2623c25c0ca1decfa7e566b636 Mon Sep 17 00:00:00 2001 From: Shockah Date: Thu, 10 Feb 2022 10:08:14 +0100 Subject: implement `out` parameter proxying --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 46 ++++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index b1b3c451..3c6d7c61 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; -using HarmonyLib; namespace StardewModdingAPI.Framework.Reflection { @@ -210,18 +209,17 @@ namespace StardewModdingAPI.Framework.Reflection 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(); - methodBuilder.SetParameters(argTypes); // proxy additional types string returnValueProxyTypeName = null; string[] parameterProxyTypeNames = new string[argTypes.Length]; if (positionsToProxy.Count > 0) { - var targetParameters = target.GetParameters(); foreach (int? position in positionsToProxy) { // we don't check for generics here, because earlier code does and generic positions won't end up here @@ -247,16 +245,21 @@ namespace StardewModdingAPI.Framework.Reflection } methodBuilder.SetReturnType(returnType); - methodBuilder.SetParameters(argTypes); } + 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[] outLocals = new LocalBuilder[argTypes.Length]; + LocalBuilder[] outInputLocals = new LocalBuilder[argTypes.Length]; + LocalBuilder[] outOutputLocals = new LocalBuilder[argTypes.Length]; // calling the proxied method - var resultLocal = il.DeclareLocal(typeof(object)); // we store both unmodified and modified in here, hence `object` + 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++) @@ -268,28 +271,35 @@ namespace StardewModdingAPI.Framework.Reflection else { // previous code already checks if the parameters are specifically `out` - outLocals[i] = il.DeclareLocal(typeof(object)); // we store both unmodified and modified in here, hence `object` - il.Emit(OpCodes.Ldloca_S, outLocals[i]); + 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); - il.Emit(OpCodes.Stloc, resultLocal); + if (target.ReturnType != typeof(void)) + il.Emit(OpCodes.Stloc, resultInputLocal); - void ProxyNonNullIfNeeded(LocalBuilder local, string proxyTypeName) + 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, local); + 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, local); + il.Emit(OpCodes.Ldloc, inputLocal); il.Emit(OpCodes.Call, CreateInstanceForProxyTypeNameMethod); - il.Emit(OpCodes.Stloc, local); + il.Emit(OpCodes.Castclass, outputLocal.LocalType); + il.Emit(OpCodes.Stloc, outputLocal); il.MarkLabel(isNullLabel); } @@ -301,17 +311,19 @@ namespace StardewModdingAPI.Framework.Reflection continue; // previous code already checks if the parameters are specifically `out` - ProxyNonNullIfNeeded(outLocals[i], parameterProxyTypeNames[i]); + ProxyIfNeededAndStore(outInputLocals[i], outOutputLocals[i], parameterProxyTypeNames[i]); il.Emit(OpCodes.Ldarg, i + 1); - il.Emit(OpCodes.Ldloc, outLocals[i]); + il.Emit(OpCodes.Ldloc, outOutputLocals[i]); il.Emit(OpCodes.Stind_Ref); } // proxying return value - ProxyNonNullIfNeeded(resultLocal, returnValueProxyTypeName); + if (target.ReturnType != typeof(void)) + ProxyIfNeededAndStore(resultInputLocal, resultOutputLocal, returnValueProxyTypeName); // return result - il.Emit(OpCodes.Ldloc, resultLocal); + if (target.ReturnType != typeof(void)) + il.Emit(OpCodes.Ldloc, resultOutputLocal); il.Emit(OpCodes.Ret); } } -- cgit From 955790842518d68ca5df21efe1beccc6069a35fe Mon Sep 17 00:00:00 2001 From: Shockah Date: Thu, 10 Feb 2022 10:18:31 +0100 Subject: fix code style warning --- src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 3c6d7c61..9573c791 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework.Reflection 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.Call, typeof(object).GetConstructor(Array.Empty())); // 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 -- cgit From 61415e41eb8f5f61e8b241255162257191c0a766 Mon Sep 17 00:00:00 2001 From: Shockah Date: Thu, 10 Feb 2022 11:21:41 +0100 Subject: use Call/Callvirt depending on target --- src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 9573c791..49cc6bca 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -276,7 +276,7 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ldloca, outInputLocals[i]); } } - il.Emit(OpCodes.Callvirt, target); + il.Emit(target.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, target); if (target.ReturnType != typeof(void)) il.Emit(OpCodes.Stloc, resultInputLocal); -- cgit From 07259452170a253c44d5c2be68fc2342a88d2504 Mon Sep 17 00:00:00 2001 From: Shockah Date: Thu, 10 Feb 2022 11:43:35 +0100 Subject: add proxy instance caching --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 18 +++++++++++++----- .../Framework/Reflection/InterfaceProxyFactory.cs | 2 +- src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 49cc6bca..63a594fa 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.CompilerServices; namespace StardewModdingAPI.Framework.Reflection { @@ -14,7 +15,7 @@ namespace StardewModdingAPI.Framework.Reflection *********/ 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) }); + private static readonly MethodInfo ObtainInstanceForProxyTypeNameMethod = typeof(InterfaceProxyGlue).GetMethod(nameof(InterfaceProxyGlue.ObtainInstanceForProxyTypeName), new Type[] { typeof(string), typeof(object) }); /********* ** Fields @@ -28,6 +29,8 @@ namespace StardewModdingAPI.Framework.Reflection /// The generated proxy type. private Type ProxyType; + /// A cache of all proxies generated by this builder. + private readonly ConditionalWeakTable ProxyCache = new(); /********* ** Public methods @@ -168,15 +171,20 @@ namespace StardewModdingAPI.Framework.Reflection this.ProxyType = proxyBuilder.CreateType(); } - /// Create an instance of the proxy for a target instance. + /// Get an existing or create a new 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) + public object ObtainInstance(object targetInstance, InterfaceProxyFactory factory) { + if (this.ProxyCache.TryGetValue(targetInstance, out object proxyInstance)) + return proxyInstance; + 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) }); + proxyInstance = constructor.Invoke(new[] { targetInstance, new InterfaceProxyGlue(factory) }); + this.ProxyCache.Add(targetInstance, proxyInstance); + return proxyInstance; } @@ -297,7 +305,7 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ldfld, glueField); il.Emit(OpCodes.Ldstr, proxyTypeName); il.Emit(OpCodes.Ldloc, i