diff options
Diffstat (limited to 'src/SMAPI/Framework/Reflection')
4 files changed, 195 insertions, 7 deletions
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); + } + } + } +} |