diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-02-25 23:21:45 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-25 23:21:45 -0500 |
commit | 512c2b9fb761838a852fb4c8fe8dff40bab5f31e (patch) | |
tree | aee12f1ea75e3ed67918573531e999d0643fd811 | |
parent | 6fc3be52bc2faaa85bea2febe6953fb5b51dac3a (diff) | |
parent | 5e3237d000aba8a4d3c7e40d5791a476fb5d6a65 (diff) | |
download | SMAPI-512c2b9fb761838a852fb4c8fe8dff40bab5f31e.tar.gz SMAPI-512c2b9fb761838a852fb4c8fe8dff40bab5f31e.tar.bz2 SMAPI-512c2b9fb761838a852fb4c8fe8dff40bab5f31e.zip |
Merge pull request #830 from Shockah/api-proxy-tryproxy-object
API proxy improvements
-rw-r--r-- | build/common.targets | 1 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 11 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 118 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs | 61 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 17 | ||||
-rw-r--r-- | src/SMAPI/SMAPI.csproj | 1 |
6 files changed, 20 insertions, 189 deletions
diff --git a/build/common.targets b/build/common.targets index 86624b62..bcb0e9e1 100644 --- a/build/common.targets +++ b/build/common.targets @@ -53,6 +53,7 @@ <Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" /> <Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" /> + <Copy SourceFiles="$(TargetDir)\Pintail.dll" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" /> <!-- Harmony + dependencies --> diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index ef1ad30c..93ea6028 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Nanoray.Pintail; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -19,7 +20,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private readonly HashSet<string> AccessedModApis = new HashSet<string>(); /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> - private readonly InterfaceProxyFactory ProxyFactory; + private readonly IProxyManager<string> ProxyManager; /********* @@ -28,13 +29,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Construct an instance.</summary> /// <param name="modID">The unique ID of the relevant mod.</param> /// <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="proxyManager">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(string modID, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor) + public ModRegistryHelper(string modID, ModRegistry registry, IProxyManager<string> proxyManager, IMonitor monitor) : base(modID) { this.Registry = registry; - this.ProxyFactory = proxyFactory; + this.ProxyManager = proxyManager; this.Monitor = monitor; } @@ -96,7 +97,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get API of type if (api is TInterface castApi) return castApi; - return this.ProxyFactory.CreateProxy<TInterface>(api, this.ModID, uniqueID); + return this.ProxyManager.ObtainProxy<string, TInterface>(api, this.ModID, uniqueID); } } } diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs deleted file mode 100644 index 70ef81f8..00000000 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ /dev/null @@ -1,118 +0,0 @@ -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 InterfaceProxyBuilder - { - /********* - ** 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 InterfaceProxyBuilder(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(new Type[0])); // call base constructor - il.Emit(OpCodes.Ldarg_0); // this - il.Emit(OpCodes.Ldarg_1); // load argument - il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument - il.Emit(OpCodes.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/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs deleted file mode 100644 index 5acba569..00000000 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> - internal class InterfaceProxyFactory - { - /********* - ** 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, InterfaceProxyBuilder> Builders = new Dictionary<string, InterfaceProxyBuilder>(); - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public InterfaceProxyFactory() - { - AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); - this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); - } - - /// <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> - 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 InterfaceProxyBuilder builder)) - { - builder = new InterfaceProxyBuilder(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 bfbd64ca..8f810644 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -48,6 +48,8 @@ using xTile.Display; using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix; using PathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; using SObject = StardewValley.Object; +using Nanoray.Pintail; +using System.Reflection.Emit; namespace StardewModdingAPI.Framework { @@ -1488,12 +1490,17 @@ namespace StardewModdingAPI.Framework { // init HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); - InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); + IProxyManager<string> proxyManager = new ProxyManager<string>(moduleBuilder, new ProxyManagerConfiguration<string>( + proxyPrepareBehavior: ProxyManagerProxyPrepareBehavior.Eager, + proxyObjectInterfaceMarking: ProxyObjectInterfaceMarking.Disabled + )); // load mods foreach (IModMetadata mod in mods) { - if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string errorPhrase, out string errorDetails)) + if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyManager, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string errorPhrase, out string errorDetails)) { failReason ??= ModFailReason.LoadFailed; mod.SetStatus(ModMetadataStatus.Failed, failReason.Value, errorPhrase, errorDetails); @@ -1596,7 +1603,7 @@ namespace StardewModdingAPI.Framework /// <param name="mod">The mod to load.</param> /// <param name="mods">The mods being loaded.</param> /// <param name="assemblyLoader">Preprocesses and loads mod assemblies.</param> - /// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param> + /// <param name="proxyManager">Generates proxy classes to access mod APIs through an arbitrary interface.</param> /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param> /// <param name="contentCore">The content manager to use for mod content.</param> /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> @@ -1605,7 +1612,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, out ModFailReason? failReason, out string errorReasonPhrase, out string errorDetails) + private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, IProxyManager<string> proxyManager, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, out ModFailReason? failReason, out string errorReasonPhrase, out string errorDetails) { errorDetails = null; @@ -1748,7 +1755,7 @@ namespace StardewModdingAPI.Framework IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack); IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection); - IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyManager, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.Multiplayer); modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index f07ede87..1540ee42 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -25,6 +25,7 @@ <PackageReference Include="Mono.Cecil" Version="0.11.4" /> <PackageReference Include="MonoMod.Common" Version="21.6.21.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> + <PackageReference Include="Pintail" Version="2.0.0" /> <PackageReference Include="Platonymous.TMXTile" Version="1.5.9" /> <PackageReference Include="System.Reflection.Emit" Version="4.7.0" /> <PackageReference Include="System.Runtime.Caching" Version="5.0.0" /> |