diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-04-16 19:15:50 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-04-16 19:15:50 -0400 |
commit | 0a69cb4bf71e6e822e595141ce5f24009e509246 (patch) | |
tree | ccdad278a6442b2aa31cee17501d4e89a4898653 | |
parent | 7dec51923418b269e111a266edb319ff3b0cb118 (diff) | |
download | SMAPI-0a69cb4bf71e6e822e595141ce5f24009e509246.tar.gz SMAPI-0a69cb4bf71e6e822e595141ce5f24009e509246.tar.bz2 SMAPI-0a69cb4bf71e6e822e595141ce5f24009e509246.zip |
allow switching between Pintail & original API proxying
-rw-r--r-- | src/SMAPI.Tests/Core/InterfaceProxyTests.cs | 68 | ||||
-rw-r--r-- | src/SMAPI.sln.DotSettings | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/Models/SConfig.cs | 10 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs | 17 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs | 10 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs | 118 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs | 57 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/SMAPI.config.json | 6 |
10 files changed, 259 insertions, 39 deletions
diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs index 8d27f6af..6be97526 100644 --- a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs +++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs @@ -29,6 +29,12 @@ namespace SMAPI.Tests.Core /// <summary>The random number generator with which to create sample values.</summary> private readonly Random Random = new(); + /// <summary>Sample user inputs for season names.</summary> + private static readonly IInterfaceProxyFactory[] ProxyFactories = { + new InterfaceProxyFactory(), + new OriginalInterfaceProxyFactory() + }; + /********* ** Unit tests @@ -37,8 +43,9 @@ namespace SMAPI.Tests.Core ** Events ****/ /// <summary>Assert that an event field can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_EventField() + public void CanProxy_EventField([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange ProviderMod providerMod = new(); @@ -46,7 +53,7 @@ namespace SMAPI.Tests.Core int expectedValue = this.Random.Next(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); new ApiConsumer().UseEventField(proxy, out Func<(int timesCalled, int lastValue)> getValues); providerMod.RaiseEvent(expectedValue); (int timesCalled, int lastValue) = getValues(); @@ -57,8 +64,9 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that an event property can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_EventProperty() + public void CanProxy_EventProperty([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange ProviderMod providerMod = new(); @@ -66,7 +74,7 @@ namespace SMAPI.Tests.Core int expectedValue = this.Random.Next(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); new ApiConsumer().UseEventProperty(proxy, out Func<(int timesCalled, int lastValue)> getValues); providerMod.RaiseEvent(expectedValue); (int timesCalled, int lastValue) = getValues(); @@ -80,10 +88,10 @@ namespace SMAPI.Tests.Core ** Properties ****/ /// <summary>Assert that properties can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> /// <param name="setVia">Whether to set the properties through the <c>provider mod</c> or <c>proxy interface</c>.</param> - [TestCase("set via provider mod")] - [TestCase("set via proxy interface")] - public void CanProxy_Properties(string setVia) + [Test] + public void CanProxy_Properties([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory, [Values("set via provider mod", "set via proxy interface")] string setVia) { // arrange ProviderMod providerMod = new(); @@ -98,7 +106,7 @@ namespace SMAPI.Tests.Core BindingFlags expectedEnum = BindingFlags.Instance | BindingFlags.Public; // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); switch (setVia) { case "set via provider mod": @@ -198,27 +206,29 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that a simple method with no return value can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_SimpleMethod_Void() + public void CanProxy_SimpleMethod_Void([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); proxy.GetNothing(); } /// <summary>Assert that a simple int method can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_SimpleMethod_Int() + public void CanProxy_SimpleMethod_Int([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); int expectedValue = this.Random.Next(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); int actualValue = proxy.GetInt(expectedValue); // assert @@ -226,15 +236,16 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that a simple object method can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_SimpleMethod_Object() + public void CanProxy_SimpleMethod_Object([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); object expectedValue = new(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); object actualValue = proxy.GetObject(expectedValue); // assert @@ -242,15 +253,16 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that a simple list method can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_SimpleMethod_List() + public void CanProxy_SimpleMethod_List([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); string expectedValue = this.GetRandomString(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); IList<string> actualValue = proxy.GetList(expectedValue); // assert @@ -258,15 +270,16 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that a simple list with interface method can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_SimpleMethod_ListWithInterface() + public void CanProxy_SimpleMethod_ListWithInterface([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); string expectedValue = this.GetRandomString(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); IList<string> actualValue = proxy.GetListWithInterface(expectedValue); // assert @@ -274,8 +287,9 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that a simple method which returns generic types can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] - public void CanProxy_SimpleMethod_GenericTypes() + public void CanProxy_SimpleMethod_GenericTypes([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); @@ -283,7 +297,7 @@ namespace SMAPI.Tests.Core string expectedValue = this.GetRandomString(); // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); IDictionary<string, IList<string>> actualValue = proxy.GetGenerics(expectedKey, expectedValue); // assert @@ -294,16 +308,17 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that a simple lambda method can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] [SuppressMessage("ReSharper", "ConvertToLocalFunction")] - public void CanProxy_SimpleMethod_Lambda() + public void CanProxy_SimpleMethod_Lambda([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); Func<string, string> expectedValue = _ => "test"; // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); object actualValue = proxy.GetObject(expectedValue); // assert @@ -311,16 +326,17 @@ namespace SMAPI.Tests.Core } /// <summary>Assert that a method with out parameters can be proxied correctly.</summary> + /// <param name="proxyFactory">The proxy factory to test.</param> [Test] [SuppressMessage("ReSharper", "ConvertToLocalFunction")] - public void CanProxy_Method_OutParameters() + public void CanProxy_Method_OutParameters([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory) { // arrange object implementation = new ProviderMod().GetModApi(); const int expectedNumber = 42; // act - ISimpleApi proxy = this.GetProxy(implementation); + ISimpleApi proxy = this.GetProxy(proxyFactory, implementation); bool result = proxy.TryGetOutParameter( inputNumber: expectedNumber, @@ -374,10 +390,10 @@ namespace SMAPI.Tests.Core } /// <summary>Get a proxy API instance.</summary> + /// <param name="proxyFactory">The proxy factory to use.</param> /// <param name="implementation">The underlying API instance.</param> - private ISimpleApi GetProxy(object implementation) + private ISimpleApi GetProxy(IInterfaceProxyFactory proxyFactory, object implementation) { - var proxyFactory = new InterfaceProxyFactory(); return proxyFactory.CreateProxy<ISimpleApi>(implementation, this.FromModId, this.ToModId); } } diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index ad546665..c8dcdb55 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -61,11 +61,13 @@ <s:Boolean x:Key="/Default/UserDictionary/Words/=Pastebin/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=pathfinding/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Pathoschild/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/UserDictionary/Words/=Pintail/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=premultiplied/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=premultiply/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Prenormalize/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Preprocesses/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=prerelease/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/UserDictionary/Words/=proxying/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=pufferchick/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=rasterizer/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=reimplements/@EntryIndexedValue">True</s:Boolean> diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 39cef758..348ba225 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private readonly HashSet<string> AccessedModApis = new(); /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> - private readonly InterfaceProxyFactory ProxyFactory; + private readonly IInterfaceProxyFactory ProxyFactory; /********* @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="registry">The underlying mod registry.</param> /// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param> /// <param name="monitor">Encapsulates monitoring and logging for the mod.</param> - public ModRegistryHelper(IModMetadata mod, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor) + public ModRegistryHelper(IModMetadata mod, ModRegistry registry, IInterfaceProxyFactory proxyFactory, IMonitor monitor) : base(mod) { this.Registry = registry; diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index d626ab4d..1a43c1fc 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -22,7 +22,8 @@ namespace StardewModdingAPI.Framework.Models [nameof(VerboseLogging)] = false, [nameof(LogNetworkTraffic)] = false, [nameof(RewriteMods)] = true, - [nameof(AggressiveMemoryOptimizations)] = false + [nameof(AggressiveMemoryOptimizations)] = false, + [nameof(UsePintail)] = true }; /// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary> @@ -64,6 +65,9 @@ namespace StardewModdingAPI.Framework.Models /// <summary>Whether to enable more aggressive memory optimizations.</summary> public bool AggressiveMemoryOptimizations { get; } + /// <summary>Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</summary> + public bool UsePintail { get; } + /// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary> public bool LogNetworkTraffic { get; } @@ -87,10 +91,11 @@ namespace StardewModdingAPI.Framework.Models /// <param name="verboseLogging">Whether SMAPI should log more information about the game context.</param> /// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param> /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> + /// <param name="usePintail">Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</param> /// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param> /// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param> /// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param> - public SConfig(bool developerMode, bool checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, bool verboseLogging, bool? rewriteMods, bool? aggressiveMemoryOptimizations, bool logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) + public SConfig(bool developerMode, bool checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, bool verboseLogging, bool? rewriteMods, bool? aggressiveMemoryOptimizations, bool usePintail, bool logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates; @@ -101,6 +106,7 @@ namespace StardewModdingAPI.Framework.Models this.VerboseLogging = verboseLogging; this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)]; this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations ?? (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)]; + this.UsePintail = usePintail; this.LogNetworkTraffic = logNetworkTraffic; this.ConsoleColors = consoleColors; this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>(); diff --git a/src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs new file mode 100644 index 00000000..6429db58 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI.Framework.Reflection +{ + /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> + internal interface IInterfaceProxyFactory + { + /********* + ** Methods + *********/ + /// <summary>Create an API proxy.</summary> + /// <typeparam name="TInterface">The interface through which to access the API.</typeparam> + /// <param name="instance">The API instance to access.</param> + /// <param name="sourceModID">The unique ID of the mod consuming the API.</param> + /// <param name="targetModID">The unique ID of the mod providing the API.</param> + TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID) + where TInterface : class; + } +} diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs index 40adde8e..694c563d 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -4,8 +4,8 @@ using Nanoray.Pintail; namespace StardewModdingAPI.Framework.Reflection { - /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> - internal class InterfaceProxyFactory + /// <inheritdoc /> + internal class InterfaceProxyFactory : IInterfaceProxyFactory { /********* ** Fields @@ -28,11 +28,7 @@ namespace StardewModdingAPI.Framework.Reflection )); } - /// <summary>Create an API proxy.</summary> - /// <typeparam name="TInterface">The interface through which to access the API.</typeparam> - /// <param name="instance">The API instance to access.</param> - /// <param name="sourceModID">The unique ID of the mod consuming the API.</param> - /// <param name="targetModID">The unique ID of the mod providing the API.</param> + /// <inheritdoc /> public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID) where TInterface : class { diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs new file mode 100644 index 00000000..9576f768 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// <summary>Generates a proxy class to access a mod API through an arbitrary interface.</summary> + internal class OriginalInterfaceProxyBuilder + { + /********* + ** Fields + *********/ + /// <summary>The target class type.</summary> + private readonly Type TargetType; + + /// <summary>The generated proxy type.</summary> + private readonly Type ProxyType; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="name">The type name to generate.</param> + /// <param name="moduleBuilder">The CLR module in which to create proxy classes.</param> + /// <param name="interfaceType">The interface type to implement.</param> + /// <param name="targetType">The target type.</param> + public OriginalInterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType) + { + // validate + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + // define proxy type + TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class); + proxyBuilder.AddInterfaceImplementation(interfaceType); + + // create field to store target instance + FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); + + // create constructor which accepts target instance and sets field + { + ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); + ILGenerator il = constructor.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); // this + // ReSharper disable once AssignNullToNotNullAttribute -- never null + il.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)!); // call base constructor + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldarg_1); // load argument + il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument + il.Emit(OpCodes.Ret); + } + + // proxy methods + foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) + { + var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray()); + if (targetMethod == null) + throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); + + this.ProxyMethod(proxyBuilder, targetMethod, targetField); + } + + // save info + this.TargetType = targetType; + this.ProxyType = proxyBuilder.CreateType()!; + } + + /// <summary>Create an instance of the proxy for a target instance.</summary> + /// <param name="targetInstance">The target instance.</param> + public object CreateInstance(object targetInstance) + { + ConstructorInfo? constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); + if (constructor == null) + throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen + return constructor.Invoke(new[] { targetInstance }); + } + + + /********* + ** Private methods + *********/ + /// <summary>Define a method which proxies access to a method on the target.</summary> + /// <param name="proxyBuilder">The proxy type being generated.</param> + /// <param name="target">The target method.</param> + /// <param name="instanceField">The proxy field containing the API instance.</param> + private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField) + { + Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray(); + + // create method + MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); + methodBuilder.SetParameters(argTypes); + methodBuilder.SetReturnType(target.ReturnType); + + // create method body + { + ILGenerator il = methodBuilder.GetILGenerator(); + + // load target instance + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, instanceField); + + // invoke target method on instance + for (int i = 0; i < argTypes.Length; i++) + il.Emit(OpCodes.Ldarg, i + 1); + il.Emit(OpCodes.Call, target); + + // return result + il.Emit(OpCodes.Ret); + } + } + } +} diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs new file mode 100644 index 00000000..d6966978 --- /dev/null +++ b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// <inheritdoc /> + internal class OriginalInterfaceProxyFactory : IInterfaceProxyFactory + { + /********* + ** Fields + *********/ + /// <summary>The CLR module in which to create proxy classes.</summary> + private readonly ModuleBuilder ModuleBuilder; + + /// <summary>The generated proxy types.</summary> + private readonly IDictionary<string, OriginalInterfaceProxyBuilder> Builders = new Dictionary<string, OriginalInterfaceProxyBuilder>(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public OriginalInterfaceProxyFactory() + { + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); + this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); + } + + /// <inheritdoc /> + public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID) + where TInterface : class + { + lock (this.Builders) + { + // validate + if (instance == null) + throw new InvalidOperationException("Can't proxy access to a null API."); + if (!typeof(TInterface).IsInterface) + throw new InvalidOperationException("The proxy type must be an interface, not a class."); + + // get proxy type + Type targetType = instance.GetType(); + string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; + if (!this.Builders.TryGetValue(proxyTypeName, out OriginalInterfaceProxyBuilder? builder)) + { + builder = new OriginalInterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType); + this.Builders[proxyTypeName] = builder; + } + + // create instance + return (TInterface)builder.CreateInstance(instance); + } + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index b3c9087f..44853627 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1560,7 +1560,9 @@ namespace StardewModdingAPI.Framework { // init HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); - InterfaceProxyFactory proxyFactory = new(); + IInterfaceProxyFactory proxyFactory = this.Settings.UsePintail + ? new InterfaceProxyFactory() + : new OriginalInterfaceProxyFactory(); // load mods foreach (IModMetadata mod in mods) @@ -1699,7 +1701,7 @@ namespace StardewModdingAPI.Framework /// <param name="errorReasonPhrase">The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable).</param> /// <param name="errorDetails">More detailed details about the error intended for developers (if any).</param> /// <returns>Returns whether the mod was successfully loaded.</returns> - private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, [NotNullWhen(false)] out ModFailReason? failReason, out string? errorReasonPhrase, out string? errorDetails) + private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, IInterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, [NotNullWhen(false)] out ModFailReason? failReason, out string? errorReasonPhrase, out string? errorDetails) { errorDetails = null; diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 49056e83..065dfa8c 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -47,6 +47,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future "AggressiveMemoryOptimizations": false, /** + * Whether to use the experimental Pintail API proxying library, instead of the original + * proxying built into SMAPI itself. + */ + "UsePintail": true, + + /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as * part of their normal functionality, so these warnings are meaningless without further |