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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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 51b7b9fe06d91d59461f9b9d6d3c1b1c736dc34d Mon Sep 17 00:00:00 2001 From: Ameisen <14104310+ameisen@users.noreply.github.com> Date: Wed, 9 Feb 2022 18:00:15 -0600 Subject: Cleanup and performance/allocation improvement for AssetDataForImage.PatchImage --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 529fb93a..c75514bc 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -41,39 +41,40 @@ namespace StardewModdingAPI.Framework.Content targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); // validate - if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) + if (!source.Bounds.Contains(sourceArea.Value)) throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) + if (!target.Bounds.Contains(targetArea.Value)) throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); - if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) + if (sourceArea.Value.Size != targetArea.Value.Size) throw new InvalidOperationException("The source and target areas must be the same size."); // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; - Color[] sourceData = new Color[pixelCount]; + Color[] sourceData = GC.AllocateUninitializedArray(pixelCount); source.GetData(0, sourceArea, sourceData, 0, pixelCount); // merge data in overlay mode if (patchMode == PatchMode.Overlay) { // get target data - Color[] targetData = new Color[pixelCount]; + Color[] targetData = GC.AllocateUninitializedArray(pixelCount); target.GetData(0, targetArea, targetData, 0, pixelCount); // merge pixels - Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; - target.GetData(0, targetArea, newData, 0, newData.Length); for (int i = 0; i < sourceData.Length; i++) { Color above = sourceData[i]; Color below = targetData[i]; // shortcut transparency - if (above.A < AssetDataForImage.MinOpacity) + if (above.A < MinOpacity) + { + sourceData[i] = below; continue; - if (below.A < AssetDataForImage.MinOpacity) + } + if (below.A < MinOpacity) { - newData[i] = above; + sourceData[i] = above; continue; } @@ -84,14 +85,13 @@ namespace StardewModdingAPI.Framework.Content // Note: don't use named arguments here since they're different between // Linux/macOS and Windows. float alphaBelow = 1 - (above.A / 255f); - newData[i] = new Color( + sourceData[i] = new Color( (int)(above.R + (below.R * alphaBelow)), // r (int)(above.G + (below.G * alphaBelow)), // g (int)(above.B + (below.B * alphaBelow)), // b Math.Max(above.A, below.A) // a ); } - sourceData = newData; } // patch target texture @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Framework.Content return false; Texture2D original = this.Data; - Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); + Texture2D texture = new(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight)); this.ReplaceWith(texture); this.PatchImage(original); return true; -- 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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/SMAPI/Framework') 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, inputLocal); - il.Emit(OpCodes.Call, CreateInstanceForProxyTypeNameMethod); + il.Emit(OpCodes.Call, ObtainInstanceForProxyTypeNameMethod); il.Emit(OpCodes.Castclass, outputLocal.LocalType); il.Emit(OpCodes.Stloc, outputLocal); diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs index 72b4254c..daeac2ad 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -42,7 +42,7 @@ namespace StardewModdingAPI.Framework.Reflection // create instance InterfaceProxyBuilder builder = this.ObtainBuilder(instance.GetType(), typeof(TInterface), sourceModID, targetModID); - return (TInterface)builder.CreateInstance(instance, this); + return (TInterface)builder.ObtainInstance(instance, this); } internal InterfaceProxyBuilder ObtainBuilder(Type targetType, Type interfaceType, string sourceModID, string targetModID) diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs index 8d0d74a7..f98b54a2 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs @@ -10,13 +10,13 @@ namespace StardewModdingAPI.Framework.Reflection this.Factory = factory; } - /// Creates a new proxied instance by its type name. + /// Get an existing or create 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) + public object ObtainInstanceForProxyTypeName(string proxyTypeName, object toProxy) { var builder = this.Factory.GetBuilderByProxyTypeName(proxyTypeName); - return builder.CreateInstance(toProxy, this.Factory); + return builder.ObtainInstance(toProxy, this.Factory); } } } -- cgit From 467375a7a37f5ed3012d1786d3149a0dddd28871 Mon Sep 17 00:00:00 2001 From: Shockah Date: Thu, 10 Feb 2022 14:15:06 +0100 Subject: add reverse API proxying (and unproxying) --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 166 +++++++++++++-------- .../Framework/Reflection/InterfaceProxyFactory.cs | 4 +- .../Framework/Reflection/InterfaceProxyGlue.cs | 12 ++ 3 files changed, 120 insertions(+), 62 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 63a594fa..81cfff50 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -16,6 +16,7 @@ namespace StardewModdingAPI.Framework.Reflection private static readonly string TargetFieldName = "__Target"; private static readonly string GlueFieldName = "__Glue"; private static readonly MethodInfo ObtainInstanceForProxyTypeNameMethod = typeof(InterfaceProxyGlue).GetMethod(nameof(InterfaceProxyGlue.ObtainInstanceForProxyTypeName), new Type[] { typeof(string), typeof(object) }); + private static readonly MethodInfo UnproxyOrObtainInstanceForProxyTypeNameMethod = typeof(InterfaceProxyGlue).GetMethod(nameof(InterfaceProxyGlue.UnproxyOrObtainInstanceForProxyTypeName), new Type[] { typeof(string), typeof(string), typeof(object) }); /********* ** Fields @@ -23,6 +24,9 @@ namespace StardewModdingAPI.Framework.Reflection /// The target class type. private readonly Type TargetType; + /// The interfce type. + private readonly Type InterfaceType; + /// The full name of the generated proxy type. private readonly string ProxyTypeName; @@ -37,26 +41,29 @@ namespace StardewModdingAPI.Framework.Reflection *********/ /// Construct an instance. /// The target type. + /// The interface type to implement. /// The type name to generate. - public InterfaceProxyBuilder(Type targetType, string proxyTypeName) + public InterfaceProxyBuilder(Type targetType, Type interfaceType, string proxyTypeName) { - // validate + // validate and store this.TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType)); + this.InterfaceType = interfaceType ?? throw new ArgumentNullException(nameof(interfaceType)); this.ProxyTypeName = proxyTypeName ?? throw new ArgumentNullException(nameof(proxyTypeName)); + if (!interfaceType.IsInterface) + throw new ArgumentException($"{nameof(interfaceType)} is not an interface."); } /// 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) + public void SetupProxyType(InterfaceProxyFactory factory, ModuleBuilder moduleBuilder, string sourceModID, string targetModID) { // define proxy type TypeBuilder proxyBuilder = moduleBuilder.DefineType(this.ProxyTypeName, TypeAttributes.Public | TypeAttributes.Class); - proxyBuilder.AddInterfaceImplementation(interfaceType); + proxyBuilder.AddInterfaceImplementation(this.InterfaceType); // create fields to store target instance and proxy factory FieldBuilder targetField = proxyBuilder.DefineField(TargetFieldName, this.TargetType, FieldAttributes.Private); @@ -100,13 +107,19 @@ namespace StardewModdingAPI.Framework.Reflection 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; + if (!proxyType.IsGenericMethodParameter) + { + if (proxyType.GetNonRefType().IsInterface) + return MatchingTypesResult.IfProxied; + if (targetType.GetNonRefType().IsInterface) + return MatchingTypesResult.IfProxied; + } + return MatchingTypesResult.False; } // proxy methods - foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) + foreach (MethodInfo proxyMethod in this.InterfaceType.GetMethods()) { var proxyMethodParameters = proxyMethod.GetParameters(); var proxyMethodGenericArguments = proxyMethod.GetGenericArguments(); @@ -144,15 +157,8 @@ namespace StardewModdingAPI.Framework.Reflection case MatchingTypesResult.True: break; case MatchingTypesResult.IfProxied: - if (proxyMethodParameters[i].IsOut) - { - positionsToProxy.Add(i); - break; - } - else - { - goto targetMethodLoopContinue; - } + positionsToProxy.Add(i); + break; } } @@ -163,7 +169,7 @@ namespace StardewModdingAPI.Framework.Reflection targetMethodLoopContinue:; } - throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); + throw new InvalidOperationException($"The {this.InterfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); proxyMethodLoopContinue:; } @@ -225,7 +231,8 @@ namespace StardewModdingAPI.Framework.Reflection // proxy additional types string returnValueProxyTypeName = null; - string[] parameterProxyTypeNames = new string[argTypes.Length]; + string[] parameterTargetToArgProxyTypeNames = new string[argTypes.Length]; + string[] parameterArgToTargetUnproxyTypeNames = new string[argTypes.Length]; if (positionsToProxy.Count > 0) { foreach (int? position in positionsToProxy) @@ -240,15 +247,18 @@ namespace StardewModdingAPI.Framework.Reflection 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(); + var targetType = targetParameters[position.Value].ParameterType; + var argType = argTypes[position.Value]; + var builder = factory.ObtainBuilder(targetType.GetNonRefType(), argType.GetNonRefType(), sourceModID, targetModID); argTypes[position.Value] = argType; - parameterProxyTypeNames[position.Value] = builder.ProxyTypeName; + parameterTargetToArgProxyTypeNames[position.Value] = builder.ProxyTypeName; + + if (!targetParameters[position.Value].IsOut) + { + var argToTargetBuilder = factory.ObtainBuilder(argType.GetNonRefType(), targetType.GetNonRefType(), sourceModID, targetModID); + parameterArgToTargetUnproxyTypeNames[position.Value] = argToTargetBuilder.ProxyTypeName; + } } } @@ -262,33 +272,10 @@ namespace StardewModdingAPI.Framework.Reflection // create method body { ILGenerator il = methodBuilder.GetILGenerator(); - LocalBuilder[] outInputLocals = new LocalBuilder[argTypes.Length]; - LocalBuilder[] outOutputLocals = new LocalBuilder[argTypes.Length]; + LocalBuilder[] inputLocals = new LocalBuilder[argTypes.Length]; + LocalBuilder[] outputLocals = 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(target.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, target); - if (target.ReturnType != typeof(void)) - il.Emit(OpCodes.Stloc, resultInputLocal); - - void ProxyIfNeededAndStore(LocalBuilder inputLocal, LocalBuilder outputLocal, string proxyTypeName) + void ProxyIfNeededAndStore(LocalBuilder inputLocal, LocalBuilder outputLocal, string proxyTypeName, string unproxyTypeName) { if (proxyTypeName == null) { @@ -303,31 +290,73 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, glueField); - il.Emit(OpCodes.Ldstr, proxyTypeName); - il.Emit(OpCodes.Ldloc, inputLocal); - il.Emit(OpCodes.Call, ObtainInstanceForProxyTypeNameMethod); + if (unproxyTypeName == null) + { + il.Emit(OpCodes.Ldstr, proxyTypeName); + il.Emit(OpCodes.Ldloc, inputLocal); + il.Emit(OpCodes.Call, ObtainInstanceForProxyTypeNameMethod); + } + else + { + il.Emit(OpCodes.Ldstr, proxyTypeName); + il.Emit(OpCodes.Ldstr, unproxyTypeName); + il.Emit(OpCodes.Ldloc, inputLocal); + il.Emit(OpCodes.Call, UnproxyOrObtainInstanceForProxyTypeNameMethod); + } il.Emit(OpCodes.Castclass, outputLocal.LocalType); il.Emit(OpCodes.Stloc, outputLocal); il.MarkLabel(isNullLabel); } + // 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 (targetParameters[i].IsOut && parameterTargetToArgProxyTypeNames[i] != null) // out parameter, proxy on the way back + { + inputLocals[i] = il.DeclareLocal(targetParameters[i].ParameterType.GetNonRefType()); + outputLocals[i] = il.DeclareLocal(argTypes[i].GetNonRefType()); + il.Emit(OpCodes.Ldloca, inputLocals[i]); + } + else if (parameterArgToTargetUnproxyTypeNames[i] != null) // normal parameter, proxy on the way in + { + inputLocals[i] = il.DeclareLocal(argTypes[i].GetNonRefType()); + outputLocals[i] = il.DeclareLocal(targetParameters[i].ParameterType.GetNonRefType()); + il.Emit(OpCodes.Ldarg, i + 1); + il.Emit(OpCodes.Stloc, inputLocals[i]); + ProxyIfNeededAndStore(inputLocals[i], outputLocals[i], parameterArgToTargetUnproxyTypeNames[i], parameterTargetToArgProxyTypeNames[i]); + il.Emit(OpCodes.Ldloc, outputLocals[i]); + } + else // normal parameter, no proxying + { + il.Emit(OpCodes.Ldarg, i + 1); + } + } + il.Emit(target.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, target); + if (target.ReturnType != typeof(void)) + il.Emit(OpCodes.Stloc, resultInputLocal); + // proxying `out` parameters for (int i = 0; i < argTypes.Length; i++) { - if (parameterProxyTypeNames[i] == null) + if (parameterTargetToArgProxyTypeNames[i] == null) + continue; + if (!targetParameters[i].IsOut) continue; - // previous code already checks if the parameters are specifically `out` - ProxyIfNeededAndStore(outInputLocals[i], outOutputLocals[i], parameterProxyTypeNames[i]); + ProxyIfNeededAndStore(inputLocals[i], outputLocals[i], parameterTargetToArgProxyTypeNames[i], null); il.Emit(OpCodes.Ldarg, i + 1); - il.Emit(OpCodes.Ldloc, outOutputLocals[i]); + il.Emit(OpCodes.Ldloc, outputLocals[i]); il.Emit(OpCodes.Stind_Ref); } // proxying return value if (target.ReturnType != typeof(void)) - ProxyIfNeededAndStore(resultInputLocal, resultOutputLocal, returnValueProxyTypeName); + ProxyIfNeededAndStore(resultInputLocal, resultOutputLocal, returnValueProxyTypeName, null); // return result if (target.ReturnType != typeof(void)) @@ -336,6 +365,23 @@ namespace StardewModdingAPI.Framework.Reflection } } + /// Try to get a target instance for a given proxy instance. + /// The proxy instance to look for. + /// The reference to store the found target instance in. + public bool TryUnproxy(object potentialProxyInstance, out object targetInstance) + { + foreach ((object cachedTargetInstance, object cachedProxyInstance) in this.ProxyCache) + { + if (object.ReferenceEquals(potentialProxyInstance, cachedProxyInstance)) + { + targetInstance = cachedTargetInstance; + return true; + } + } + targetInstance = null; + return false; + } + /// The part of a method that is being matched. private enum MethodTypeMatchingPart { diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs index daeac2ad..a6f38c3a 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -57,11 +57,11 @@ 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(targetType, proxyTypeName); + builder = new InterfaceProxyBuilder(targetType, interfaceType, proxyTypeName); this.Builders[proxyTypeName] = builder; try { - builder.SetupProxyType(this, this.ModuleBuilder, interfaceType, sourceModID, targetModID); + builder.SetupProxyType(this, this.ModuleBuilder, sourceModID, targetModID); } catch { diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs index f98b54a2..38569efa 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs @@ -18,5 +18,17 @@ namespace StardewModdingAPI.Framework.Reflection var builder = this.Factory.GetBuilderByProxyTypeName(proxyTypeName); return builder.ObtainInstance(toProxy, this.Factory); } + + /// Try to unproxy, or get an existing, or create a new proxied instance by its type name. + /// The full name of the proxy type. + /// The full name of the reverse proxy type. + /// The target instance to proxy. + public object UnproxyOrObtainInstanceForProxyTypeName(string proxyTypeName, string unproxyTypeName, object toProxy) + { + var unproxyBuilder = this.Factory.GetBuilderByProxyTypeName(unproxyTypeName); + if (unproxyBuilder.TryUnproxy(toProxy, out object targetInstance)) + return targetInstance; + return this.ObtainInstanceForProxyTypeName(proxyTypeName, toProxy); + } } } -- cgit From a54d58d064df5654e845f9b2b4fc9db5b0189db0 Mon Sep 17 00:00:00 2001 From: Shockah Date: Thu, 10 Feb 2022 16:26:43 +0100 Subject: add TryProxy for any objects --- .../Framework/ModHelpers/ModRegistryHelper.cs | 26 ++++++++++++++++++++++ src/SMAPI/IModRegistry.cs | 7 ++++++ 2 files changed, 33 insertions(+) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index ef1ad30c..92c52b00 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -98,5 +98,31 @@ namespace StardewModdingAPI.Framework.ModHelpers return castApi; return this.ProxyFactory.CreateProxy(api, this.ModID, uniqueID); } + + /// + public bool TryProxy(string uniqueID, object toProxy, out TInterface proxy) where TInterface : class + { + try + { + foreach (var toProxyInterface in toProxy.GetType().GetInterfaces()) + { + var unproxyBuilder = this.ProxyFactory.ObtainBuilder(typeof(TInterface), toProxyInterface, this.ModID, uniqueID); + if (unproxyBuilder.TryUnproxy(toProxy, out object targetInstance)) + { + proxy = (TInterface)targetInstance; + return true; + } + } + + var proxyBuilder = this.ProxyFactory.ObtainBuilder(toProxy.GetType(), typeof(TInterface), this.ModID, uniqueID); + proxy = (TInterface)proxyBuilder.ObtainInstance(toProxy, this.ProxyFactory); + return true; + } + catch + { + proxy = null; + return false; + } + } } } diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index 10b3121e..9b99e459 100644 --- a/src/SMAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs @@ -25,5 +25,12 @@ namespace StardewModdingAPI /// The interface which matches the properties and methods you intend to access. /// The mod's unique ID. TInterface GetApi(string uniqueID) where TInterface : class; + + /// Try to proxy (or unproxy back) the given object to a given interface provided by a mod. + /// The interface type to proxy (or unproxy) to. + /// The mod's unique ID. + /// The object to try to proxy (or unproxy back). + /// The reference to store the proxied (or unproxied) object back. + bool TryProxy(string uniqueID, object toProxy, out TInterface proxy) where TInterface : class; } } -- cgit From 4da9e954df3846d01aa0536f4e8143466a1d62f3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 Feb 2022 00:49:49 -0500 Subject: use Array.Empty to avoid unneeded array allocations --- src/SMAPI.Internal/ExceptionHelper.cs | 2 +- src/SMAPI.Mods.ErrorHandler/ModEntry.cs | 2 +- src/SMAPI.Tests/Core/ModResolverTests.cs | 16 ++++++++-------- .../Framework/Clients/WebApi/ModEntryModel.cs | 4 +++- .../Framework/Clients/WebApi/ModExtendedMetadataModel.cs | 3 ++- .../Framework/Clients/WebApi/ModSearchEntryModel.cs | 4 +++- .../Framework/Clients/Wiki/ChangeDescriptor.cs | 2 +- src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs | 2 +- .../Framework/Clients/Wiki/WikiDataOverrideEntry.cs | 4 +++- src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs | 2 +- src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 7 ++++--- src/SMAPI.Web/Controllers/IndexController.cs | 2 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- .../Framework/Caching/Wiki/WikiCacheMemoryRepository.cs | 2 +- src/SMAPI.Web/Framework/Clients/GenericModPage.cs | 3 ++- src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs | 2 +- .../ViewModels/JsonValidator/JsonValidatorModel.cs | 2 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 2 +- src/SMAPI.Web/Views/Mods/Index.cshtml | 2 +- src/SMAPI/Events/ButtonsChangedEventArgs.cs | 2 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 2 +- src/SMAPI/Framework/Models/SConfig.cs | 4 ++-- src/SMAPI/Framework/SCore.cs | 6 +++--- .../FieldWatchers/ImmutableCollectionWatcher.cs | 5 +++-- src/SMAPI/Framework/StateTracking/LocationTracker.cs | 3 ++- .../Framework/StateTracking/Snapshots/PlayerSnapshot.cs | 2 +- src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs | 2 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 +- src/SMAPI/Utilities/Keybind.cs | 4 ++-- src/SMAPI/Utilities/KeybindList.cs | 4 ++-- 30 files changed, 56 insertions(+), 45 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.Internal/ExceptionHelper.cs b/src/SMAPI.Internal/ExceptionHelper.cs index 05b96c2e..03d48911 100644 --- a/src/SMAPI.Internal/ExceptionHelper.cs +++ b/src/SMAPI.Internal/ExceptionHelper.cs @@ -25,7 +25,7 @@ namespace StardewModdingAPI.Internal case ReflectionTypeLoadException ex: string summary = ex.ToString(); - foreach (Exception childEx in ex.LoaderExceptions ?? new Exception[0]) + foreach (Exception childEx in ex.LoaderExceptions ?? Array.Empty()) summary += $"\n\n{childEx?.GetLogSummary()}"; message = summary; break; diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index 7286e316..2d6242cf 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler MethodInfo getMonitorForGame = coreType.GetMethod("GetMonitorForGame") ?? throw new InvalidOperationException("Can't access the SMAPI's 'GetMonitorForGame' method. This mod may not work correctly."); - return (IMonitor)getMonitorForGame.Invoke(core, new object[0]) ?? this.Monitor; + return (IMonitor)getMonitorForGame.Invoke(core, Array.Empty()) ?? this.Monitor; } } } diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index da3446bb..1755f644 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -123,7 +123,7 @@ namespace SMAPI.Tests.Core [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] public void ValidateManifests_NoMods_DoesNothing() { - new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(Array.Empty(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] @@ -144,7 +144,7 @@ namespace SMAPI.Tests.Core public void ValidateManifests_ModStatus_AssumeBroken_Fails() { // arrange - Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + Mock mock = this.GetMetadata("Mod A", Array.Empty(), allowStatusChange: true); this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields { Status = ModStatus.AssumeBroken @@ -161,7 +161,7 @@ namespace SMAPI.Tests.Core public void ValidateManifests_MinimumApiVersion_Fails() { // arrange - Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + Mock mock = this.GetMetadata("Mod A", Array.Empty(), allowStatusChange: true); mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1")); this.SetupMetadataForValidation(mock); @@ -190,9 +190,9 @@ namespace SMAPI.Tests.Core public void ValidateManifests_DuplicateUniqueID_Fails() { // arrange - Mock modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + Mock modA = this.GetMetadata("Mod A", Array.Empty(), allowStatusChange: true); Mock modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true); - Mock modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false); + Mock modC = this.GetMetadata("Mod C", Array.Empty(), allowStatusChange: false); foreach (Mock mod in new[] { modA, modB, modC }) this.SetupMetadataForValidation(mod); @@ -236,7 +236,7 @@ namespace SMAPI.Tests.Core public void ProcessDependencies_NoMods_DoesNothing() { // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0], new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(Array.Empty(), new ModDatabase()).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods."); @@ -490,8 +490,8 @@ namespace SMAPI.Tests.Core EntryDll = entryDll ?? $"{Sample.String()}.dll", ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, - Dependencies = dependencies ?? new IManifestDependency[0], - UpdateKeys = new string[0] + Dependencies = dependencies ?? Array.Empty(), + UpdateKeys = Array.Empty() }; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 2f58a3f1..0115fbf3 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Metadata about a mod. @@ -16,6 +18,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public ModExtendedMetadataModel Metadata { get; set; } /// The errors that occurred while fetching update data. - public string[] Errors { get; set; } = new string[0]; + public string[] Errors { get; set; } = Array.Empty(); } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 5c2ce366..0fa4a74d 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; @@ -17,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Mod info ****/ /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). - public string[] ID { get; set; } = new string[0]; + public string[] ID { get; set; } = Array.Empty(); /// The mod's display name. public string Name { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index bf81e102..404d4618 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Specifies the identifiers for a mod to match. @@ -37,7 +39,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { this.ID = id; this.InstalledVersion = installedVersion; - this.UpdateKeys = updateKeys ?? new string[0]; + this.UpdateKeys = updateKeys ?? Array.Empty(); } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs index f1feb44b..2ed255c8 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs @@ -179,7 +179,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki errors = rawErrors.ToArray(); } else - errors = new string[0]; + errors = Array.Empty(); // build model return new ChangeDescriptor( diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index f85e82e1..0f5a0ec3 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -239,7 +239,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string raw = this.GetAttribute(element, name); return !string.IsNullOrWhiteSpace(raw) ? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() - : new string[0]; + : Array.Empty(); } /// Get an attribute value and parse it as an enum value. diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs index 0587e09d..03c0d214 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs @@ -1,3 +1,5 @@ +using System; + #nullable enable namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki @@ -9,7 +11,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki ** Accessors *********/ /// The unique mod IDs for the mods to override. - public string[] Ids { get; set; } = new string[0]; + public string[] Ids { get; set; } = Array.Empty(); /// Maps local versions to a semantic version for update checks. public ChangeDescriptor? ChangeLocalVersions { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs index a9da884a..5b7e2a02 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData *********/ /// Construct an empty instance. public ModDatabase() - : this(new ModDataRecord[0], key => null) { } + : this(Array.Empty(), key => null) { } /// Construct an instance. /// The underlying mod data records indexed by default display name. diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 46b654a5..4ad97b6d 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; @@ -68,7 +69,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models this.Description = description; this.Version = version; this.UniqueID = uniqueID; - this.UpdateKeys = new string[0]; + this.UpdateKeys = Array.Empty(); this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; } @@ -77,8 +78,8 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models [OnDeserialized] public void OnDeserialized(StreamingContext context) { - this.Dependencies ??= new IManifestDependency[0]; - this.UpdateKeys ??= new string[0]; + this.Dependencies ??= Array.Empty(); + this.UpdateKeys ??= Array.Empty(); } } } diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index f2f4c342..5097997c 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -96,7 +96,7 @@ namespace StardewModdingAPI.Web.Controllers { HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(release.Body); - foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? new HtmlNode[0]) + foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? Array.Empty()) node.Remove(); release.Body = doc.DocumentNode.InnerHtml.Trim(); } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 37d763cc..dfe2504b 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -79,7 +79,7 @@ namespace StardewModdingAPI.Web.Controllers public async Task> PostAsync([FromBody] ModSearchModel model, [FromRoute] string version) { if (model?.Mods == null) - return new ModEntryModel[0]; + return Array.Empty(); ModUpdateCheckConfig config = this.Config.Value; diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs index 064a7c3c..d037a123 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs @@ -15,7 +15,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki private Cached Metadata; /// The cached wiki data. - private Cached[] Mods = new Cached[0]; + private Cached[] Mods = Array.Empty>(); /********* diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 622e6c56..a5f7c9b9 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -26,7 +27,7 @@ namespace StardewModdingAPI.Web.Framework.Clients public string Url { get; set; } /// The mod downloads. - public IModDownload[] Downloads { get; set; } = new IModDownload[0]; + public IModDownload[] Downloads { get; set; } = Array.Empty(); /// The mod availability status on the remote site. public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs index 87b20eb0..693a16ec 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs @@ -42,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models public DateTimeOffset Timestamp { get; set; } /// Metadata about installed mods and content packs. - public LogModInfo[] Mods { get; set; } = new LogModInfo[0]; + public LogModInfo[] Mods { get; set; } = Array.Empty(); /// The log messages. public LogMessage[] Messages { get; set; } diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index 0ea69911..e659b389 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator public string Content { get; set; } /// The schema validation errors, if any. - public JsonValidatorErrorModel[] Errors { get; set; } = new JsonValidatorErrorModel[0]; + public JsonValidatorErrorModel[] Errors { get; set; } = Array.Empty(); /// A non-blocking warning while uploading the file. public string UploadWarning { get; set; } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 91fc3535..993e7244 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -15,7 +15,7 @@ string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true); - ISet screenIds = new HashSet(Model.ParsedLog?.Messages?.Select(p => p.ScreenId) ?? new int[0]); + ISet screenIds = new HashSet(Model.ParsedLog?.Messages?.Select(p => p.ScreenId) ?? Array.Empty()); } @section Head { diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 8a764803..416468e4 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -19,7 +19,7 @@