using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace StardewModdingAPI.Framework.Reflection
{
/// Generates proxy classes to access mod APIs through an arbitrary interface.
internal class InterfaceProxyBuilder
{
/*********
** Properties
*********/
/// The CLR module in which to create proxy classes.
private readonly ModuleBuilder ModuleBuilder;
/// The generated proxy types.
private readonly IDictionary GeneratedTypes = new Dictionary();
/*********
** Public methods
*********/
/// Construct an instance.
public InterfaceProxyBuilder()
{
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
}
/// Create an API proxy.
/// The interface through which to access the API.
/// The API instance to access.
/// The unique ID of the mod consuming the API.
/// 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.");
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.GeneratedTypes.TryGetValue(proxyTypeName, out Type type))
{
type = this.CreateProxyType(proxyTypeName, typeof(TInterface), targetType);
this.GeneratedTypes[proxyTypeName] = type;
}
// create instance
ConstructorInfo constructor = type.GetConstructor(new[] { targetType });
if (constructor == null)
throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen
return (TInterface)constructor.Invoke(new[] { instance });
}
/*********
** Private methods
*********/
/// Define a class which proxies access to a target type through an interface.
/// The name of the proxy type to generate.
/// The interface type through which to access the target.
/// The target type to access.
private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType)
{
// define proxy type
TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class);
proxyBuilder.AddInterfaceImplementation(interfaceType);
// create field to store target instance
FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private);
// create constructor which accepts target instance
{
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, field); // 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, field);
}
// create type
return proxyBuilder.CreateType();
}
/// Define a method which proxies access to a method on the target.
/// The proxy type being generated.
/// The target method.
/// The proxy field containing the API instance.
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);
}
}
}
}