using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace Umbraco.Core
{
///
/// Provides utilities to simplify reflection.
///
///
/// Readings:
/// * CIL instructions: https://en.wikipedia.org/wiki/List_of_CIL_instructions
/// * ECMA 335: https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf
/// * MSIL programming: http://www.blackbeltcoder.com/Articles/net/msil-programming-part-1
///
/// Supports emitting constructors, instance and static methods, instance property getters and
/// setters. Does not support static properties yet.
///
public static partial class ReflectionUtilities
{
#region Fields
///
/// Emits a field getter.
///
/// The declaring type.
/// The field type.
/// The name of the field.
///
/// A field getter function.
///
/// fieldName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Value type does not match field . type.
/// Could not find field ..
public static Func EmitFieldGetter(string fieldName)
{
var field = GetField(fieldName);
return EmitFieldGetter(field);
}
///
/// Emits a field setter.
///
/// The declaring type.
/// The field type.
/// The name of the field.
///
/// A field setter action.
///
/// fieldName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Value type does not match field . type.
/// Could not find field ..
public static Action EmitFieldSetter(string fieldName)
{
var field = GetField(fieldName);
return EmitFieldSetter(field);
}
///
/// Emits a field getter and setter.
///
/// The declaring type.
/// The field type.
/// The name of the field.
///
/// A field getter and setter functions.
///
/// fieldName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Value type does not match field . type.
/// Could not find field ..
public static (Func, Action) EmitFieldGetterAndSetter(string fieldName)
{
var field = GetField(fieldName);
return (EmitFieldGetter(field), EmitFieldSetter(field));
}
///
/// Gets the field.
///
/// The type of the declaring.
/// The type of the value.
/// Name of the field.
///
/// fieldName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Value type does not match field . type.
/// Could not find field ..
private static FieldInfo GetField(string fieldName)
{
if (fieldName == null) throw new ArgumentNullException(nameof(fieldName));
if (string.IsNullOrWhiteSpace(fieldName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(fieldName));
// get the field
var field = typeof(TDeclaring).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field == null) throw new InvalidOperationException($"Could not find field {typeof(TDeclaring)}.{fieldName}.");
// validate field type
if (field.FieldType != typeof(TValue)) // strict
throw new ArgumentException($"Value type {typeof(TValue)} does not match field {typeof(TDeclaring)}.{fieldName} type {field.FieldType}.");
return field;
}
private static Func EmitFieldGetter(FieldInfo field)
{
// emit
var (dm, ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new [] { typeof(TDeclaring) }, typeof(TValue));
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldfld, field);
ilgen.Return();
return (Func) (object) dm.CreateDelegate(typeof(Func));
}
private static Action EmitFieldSetter(FieldInfo field)
{
// emit
var (dm, ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new [] { typeof(TDeclaring), typeof(TValue) }, typeof(void));
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Ldarg_1);
ilgen.Emit(OpCodes.Stfld, field);
ilgen.Return();
return (Action) (object) dm.CreateDelegate(typeof(Action));
}
#endregion
#region Properties
///
/// Emits a property getter.
///
/// The declaring type.
/// The property type.
/// The name of the property.
/// A value indicating whether the property and its getter must exist.
///
/// A property getter function. If is false, returns null when the property or its getter does not exist.
///
/// propertyName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Value type does not match property . type.
/// Could not find property getter for ..
public static Func EmitPropertyGetter(string propertyName, bool mustExist = true)
{
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName));
var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property?.GetMethod != null)
return EmitMethod>(property.GetMethod);
if (!mustExist)
return default;
throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}.");
}
///
/// Emits a property setter.
///
/// The declaring type.
/// The property type.
/// The name of the property.
/// A value indicating whether the property and its setter must exist.
///
/// A property setter function. If is false, returns null when the property or its setter does not exist.
///
/// propertyName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Value type does not match property . type.
/// Could not find property setter for ..
public static Action EmitPropertySetter(string propertyName, bool mustExist = true)
{
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName));
var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property?.SetMethod != null)
return EmitMethod>(property.SetMethod);
if (!mustExist)
return default;
throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}.");
}
///
/// Emits a property getter and setter.
///
/// The declaring type.
/// The property type.
/// The name of the property.
/// A value indicating whether the property and its getter and setter must exist.
///
/// A property getter and setter functions. If is false, returns null when the property or its getter or setter does not exist.
///
/// propertyName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Value type does not match property . type.
/// Could not find property getter and setter for ..
public static (Func, Action) EmitPropertyGetterAndSetter(string propertyName, bool mustExist = true)
{
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName));
var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property?.GetMethod != null && property.SetMethod != null)
return (
EmitMethod>(property.GetMethod),
EmitMethod>(property.SetMethod));
if (!mustExist)
return default;
throw new InvalidOperationException($"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}.");
}
///
/// Emits a property getter.
///
/// The declaring type.
/// The property type.
/// The property info.
/// A property getter function.
/// Occurs when is null.
/// Occurs when the property has no getter.
/// Occurs when does not match the type of the property.
public static Func EmitPropertyGetter(PropertyInfo propertyInfo)
{
if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo));
if (propertyInfo.GetMethod == null)
throw new ArgumentException("Property has no getter.", nameof(propertyInfo));
return EmitMethod>(propertyInfo.GetMethod);
}
///
/// Emits a property setter.
///
/// The declaring type.
/// The property type.
/// The property info.
/// A property setter function.
/// Occurs when is null.
/// Occurs when the property has no setter.
/// Occurs when does not match the type of the property.
public static Action EmitPropertySetter(PropertyInfo propertyInfo)
{
if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo));
if (propertyInfo.SetMethod == null)
throw new ArgumentException("Property has no setter.", nameof(propertyInfo));
return EmitMethod>(propertyInfo.SetMethod);
}
///
/// Emits a property getter and setter.
///
/// The declaring type.
/// The property type.
/// The property info.
/// A property getter and setter functions.
/// Occurs when is null.
/// Occurs when the property has no getter or no setter.
/// Occurs when does not match the type of the property.
public static (Func, Action) EmitPropertyGetterAndSetter(PropertyInfo propertyInfo)
{
if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo));
if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null)
throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo));
return (
EmitMethod>(propertyInfo.GetMethod),
EmitMethod>(propertyInfo.SetMethod));
}
///
/// Emits a property setter.
///
/// The declaring type.
/// The property type.
/// The property info.
/// A property setter function.
/// Occurs when is null.
/// Occurs when the property has no setter.
/// Occurs when does not match the type of the property.
public static Action EmitPropertySetterUnsafe(PropertyInfo propertyInfo)
{
if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo));
if (propertyInfo.SetMethod == null)
throw new ArgumentException("Property has no setter.", nameof(propertyInfo));
return EmitMethodUnsafe>(propertyInfo.SetMethod);
}
#endregion
#region Constructors
///
/// Emits a constructor.
///
/// A lambda representing the constructor.
/// A value indicating whether the constructor must exist.
/// The optional type of the class to construct.
/// A constructor function. If is false, returns null when the constructor does not exist.
///
/// When is not specified, it is the type returned by .
/// The constructor arguments are determined by generic arguments.
/// The type returned by does not need to be exactly ,
/// when e.g. that type is not known at compile time, but it has to be a parent type (eg an interface, or object).
///
/// Occurs when the constructor does not exist and is true.
/// Occurs when is not a Func or when
/// is specified and does not match the function's returned type.
public static TLambda EmitConstructor(bool mustExist = true, Type declaring = null)
{
var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true);
// determine returned / declaring type
if (declaring == null) declaring = lambdaReturned;
else if (!lambdaReturned.IsAssignableFrom(declaring))
throw new ArgumentException($"Type {lambdaReturned} is not assignable from type {declaring}.", nameof(declaring));
// get the constructor infos
var ctor = declaring.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null);
if (ctor == null)
{
if (!mustExist) return default;
throw new InvalidOperationException($"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable) lambdaParameters)}).");
}
// emit
return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor);
}
///
/// Emits a constructor.
///
/// A lambda representing the constructor.
/// The constructor info.
/// A constructor function.
/// Occurs when is not a Func or when its generic
/// arguments do not match those of .
/// Occurs when is null.
public static TLambda EmitConstructor(ConstructorInfo ctor)
{
if (ctor == null) throw new ArgumentNullException(nameof(ctor));
var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true);
return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor);
}
private static TLambda EmitConstructorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor)
{
// get type and args
var ctorDeclaring = ctor.DeclaringType;
var ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray();
// validate arguments
if (lambdaParameters.Length != ctorParameters.Length)
ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters);
for (var i = 0; i < lambdaParameters.Length; i++)
if (lambdaParameters[i] != ctorParameters[i]) // note: relax the constraint with IsAssignableFrom?
ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters);
if (!returned.IsAssignableFrom(ctorDeclaring))
ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters);
// emit
return EmitConstructor(ctorDeclaring, ctorParameters, ctor);
}
///
/// Emits a constructor.
///
/// A lambda representing the constructor.
/// The constructor info.
/// A constructor function.
///
/// The constructor is emitted in an unsafe way, using the lambda arguments without verifying
/// them at all. This assumes that the calling code is taking care of all verifications, in order
/// to avoid cast errors.
///
/// Occurs when is not a Func or when its generic
/// arguments do not match those of .
/// Occurs when is null.
public static TLambda EmitConstructorUnsafe(ConstructorInfo ctor)
{
if (ctor == null) throw new ArgumentNullException(nameof(ctor));
var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true);
// emit - unsafe - use lambda's args and assume they are correct
return EmitConstructor(lambdaReturned, lambdaParameters, ctor);
}
private static TLambda EmitConstructor(Type declaring, Type[] lambdaParameters, ConstructorInfo ctor)
{
// gets the method argument types
var ctorParameters = GetParameters(ctor);
// emit
var (dm, ilgen) = CreateIlGenerator(ctor.DeclaringType?.Module, lambdaParameters, declaring);
EmitLdargs(ilgen, lambdaParameters, ctorParameters);
ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects
ilgen.Return();
return (TLambda) (object) dm.CreateDelegate(typeof(TLambda));
}
#endregion
#region Methods
///
/// Emits a static method.
///
/// The declaring type.
/// A lambda representing the method.
/// The name of the method.
/// A value indicating whether the constructor must exist.
///
/// The method. If is false, returns null when the method does not exist.
///
/// methodName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Occurs when does not match the method signature..
/// Occurs when no proper method with name could be found.
///
/// The method arguments are determined by generic arguments.
///
public static TLambda EmitMethod(string methodName, bool mustExist = true)
{
return EmitMethod(typeof(TDeclaring), methodName, mustExist);
}
///
/// Emits a static method.
///
/// A lambda representing the method.
/// The declaring type.
/// The name of the method.
/// A value indicating whether the constructor must exist.
///
/// The method. If is false, returns null when the method does not exist.
///
/// methodName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Occurs when does not match the method signature..
/// Occurs when no proper method with name could be found.
///
/// The method arguments are determined by generic arguments.
///
public static TLambda EmitMethod(Type declaring, string methodName, bool mustExist = true)
{
if (methodName == null) throw new ArgumentNullException(nameof(methodName));
if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName));
var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, out var isFunction);
// get the method infos
var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, lambdaParameters, null);
if (method == null || isFunction && !lambdaReturned.IsAssignableFrom(method.ReturnType))
{
if (!mustExist) return default;
throw new InvalidOperationException($"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)}).");
}
// emit
return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method);
}
///
/// Emits a method.
///
/// A lambda representing the method.
/// The method info.
/// The method.
/// Occurs when is null.
/// Occurs when Occurs when does not match the method signature.
public static TLambda EmitMethod(MethodInfo method)
{
if (method == null) throw new ArgumentNullException(nameof(method));
// get type and args
var methodDeclaring = method.DeclaringType;
var methodReturned = method.ReturnType;
var methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray();
var isStatic = method.IsStatic;
var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(isStatic, out var isFunction);
// if not static, then the first lambda arg must be the method declaring type
if (!isStatic && (methodDeclaring == null || !methodDeclaring.IsAssignableFrom(lambdaDeclaring)))
ThrowInvalidLambda(method.Name, methodReturned, methodParameters);
if (methodParameters.Length != lambdaParameters.Length)
ThrowInvalidLambda(method.Name, methodReturned, methodParameters);
for (var i = 0; i < methodParameters.Length; i++)
if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i]))
ThrowInvalidLambda(method.Name, methodReturned, methodParameters);
// if it's a function then the last lambda arg must match the method returned type
if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned))
ThrowInvalidLambda(method.Name, methodReturned, methodParameters);
// emit
return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method);
}
///
/// Emits a method.
///
/// A lambda representing the method.
/// The method info.
/// The method.
/// Occurs when is null.
/// Occurs when Occurs when does not match the method signature.
public static TLambda EmitMethodUnsafe(MethodInfo method)
{
if (method == null) throw new ArgumentNullException(nameof(method));
var isStatic = method.IsStatic;
var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(isStatic, out _);
// emit - unsafe - use lambda's args and assume they are correct
return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method);
}
///
/// Emits an instance method.
///
/// A lambda representing the method.
/// The name of the method.
/// A value indicating whether the constructor must exist.
///
/// The method. If is false, returns null when the method does not exist.
///
/// methodName
/// Value can't be empty or consist only of white-space characters. -
/// or
/// Occurs when does not match the method signature..
/// Occurs when no proper method with name could be found.
///
/// The method arguments are determined by generic arguments.
///
public static TLambda EmitMethod(string methodName, bool mustExist = true)
{
if (methodName == null) throw new ArgumentNullException(nameof(methodName));
if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName));
// validate lambda type
var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(false, out var isFunction);
// get the method infos
var method = lambdaDeclaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null);
if (method == null || isFunction && method.ReturnType != lambdaReturned)
{
if (!mustExist) return default;
throw new InvalidOperationException($"Could not find method {lambdaDeclaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)}).");
}
// emit
return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method);
}
// lambdaReturned = the lambda returned type (can be void)
// lambdaArgTypes = the lambda argument types
private static TLambda EmitMethod(Type lambdaDeclaring, Type lambdaReturned, Type[] lambdaParameters, MethodInfo method)
{
// non-static methods need the declaring type as first arg
var parameters = lambdaParameters;
if (!method.IsStatic)
{
parameters = new Type[lambdaParameters.Length + 1];
parameters[0] = lambdaDeclaring ?? method.DeclaringType;
Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length);
}
// gets the method argument types
var methodArgTypes = GetParameters(method, withDeclaring: !method.IsStatic);
// emit IL
var (dm, ilgen) = CreateIlGenerator(method.DeclaringType?.Module, parameters, lambdaReturned);
EmitLdargs(ilgen, parameters, methodArgTypes);
ilgen.CallMethod(method);
EmitOutputAdapter(ilgen, lambdaReturned, method.ReturnType);
ilgen.Return();
// create
return (TLambda) (object) dm.CreateDelegate(typeof(TLambda));
}
#endregion
#region Utilities
// when !isStatic, the first generic argument of the lambda is the declaring type
// hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument
// when isFunction, the last generic argument of the lambda is the returned type
// everything in between is parameters
private static (Type Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, bool isFunction)
{
var typeLambda = typeof(TLambda);
var (declaring, parameters, returned) = AnalyzeLambda(isStatic, out var maybeFunction);
if (isFunction)
{
if (!maybeFunction)
throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda));
}
else
{
if (maybeFunction)
throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda));
}
return (declaring, parameters, returned);
}
// when !isStatic, the first generic argument of the lambda is the declaring type
// hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument
// when isFunction, the last generic argument of the lambda is the returned type
// everything in between is parameters
private static (Type Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, out bool isFunction)
{
isFunction = false;
var typeLambda = typeof(TLambda);
var isAction = typeLambda.FullName == "System.Action";
if (isAction)
{
if (!isStatic)
throw new ArgumentException($"Lambda {typeLambda} is an Action and can be used for static methods exclusively.", nameof(TLambda));
return (null, Array.Empty(), typeof(void));
}
var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null;
var name = genericDefinition?.FullName;
if (name == null)
throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda));
var isActionOf = name.StartsWith("System.Action`");
isFunction = name.StartsWith("System.Func`");
if (!isActionOf && !isFunction)
throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda));
var genericArgs = typeLambda.GetGenericArguments();
if (genericArgs.Length == 0)
throw new Exception("Panic: Func<> or Action<> has zero generic arguments.");
var i = 0;
var declaring = isStatic ? typeof(void) : genericArgs[i++];
var parameterCount = genericArgs.Length - (isStatic ? 0 : 1) - (isFunction ? 1 : 0);
if (parameterCount < 0)
throw new ArgumentException($"Lambda {typeLambda} is a Func and requires at least two arguments (declaring type and returned type).", nameof(TLambda));
var parameters = new Type[parameterCount];
for (var j = 0; j < parameterCount; j++)
parameters[j] = genericArgs[i++];
var returned = isFunction ? genericArgs[i] : typeof(void);
return (declaring, parameters, returned);
}
private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module module, Type[] arguments, Type returned)
{
if (module == null) throw new ArgumentNullException(nameof(module));
var dm = new DynamicMethod(string.Empty, returned, arguments, module, true);
return (dm, dm.GetILGenerator());
}
private static Type[] GetParameters(ConstructorInfo ctor)
{
var parameters = ctor.GetParameters();
var types = new Type[parameters.Length];
var i = 0;
foreach (var parameter in parameters)
types[i++] = parameter.ParameterType;
return types;
}
private static Type[] GetParameters(MethodInfo method, bool withDeclaring)
{
var parameters = method.GetParameters();
var types = new Type[parameters.Length + (withDeclaring ? 1 : 0)];
var i = 0;
if (withDeclaring)
types[i++] = method.DeclaringType;
foreach (var parameter in parameters)
types[i++] = parameter.ParameterType;
return types;
}
// emits args
private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes)
{
var ldargOpCodes = new[] { OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 };
if (lambdaArgTypes.Length != methodArgTypes.Length)
throw new Exception("Panic: inconsistent number of args.");
for (var i = 0; i < lambdaArgTypes.Length; i++)
{
if (lambdaArgTypes.Length < 5)
ilgen.Emit(ldargOpCodes[i]);
else
ilgen.Emit(OpCodes.Ldarg, i);
//var local = false;
EmitInputAdapter(ilgen, lambdaArgTypes[i], methodArgTypes[i]/*, ref local*/);
}
}
// emits adapter opcodes after OpCodes.Ldarg
// inputType is the lambda input type
// methodParamType is the actual type expected by the actual method
// adding code to do inputType -> methodParamType
// valueType -> valueType : not supported ('cos, why?)
// valueType -> !valueType : not supported ('cos, why?)
// !valueType -> valueType : unbox and convert
// !valueType -> !valueType : cast (could throw)
private static void EmitInputAdapter(ILGenerator ilgen, Type inputType, Type methodParamType /*, ref bool local*/)
{
if (inputType == methodParamType) return;
if (methodParamType.IsValueType)
{
if (inputType.IsValueType)
{
// both input and parameter are value types
// not supported, use proper input
// (otherwise, would require converting)
throw new NotSupportedException("ValueTypes conversion.");
}
// parameter is value type, but input is reference type
// unbox the input to the parameter value type
// this is more or less equivalent to the ToT method below
var unbox = ilgen.DefineLabel();
//if (!local)
//{
// ilgen.DeclareLocal(typeof(object)); // declare local var for st/ld loc_0
// local = true;
//}
// stack: value
// following code can be replaced with .Dump (and then we don't need the local variable anymore)
//ilgen.Emit(OpCodes.Stloc_0); // pop value into loc.0
//// stack:
//ilgen.Emit(OpCodes.Ldloc_0); // push loc.0
//ilgen.Emit(OpCodes.Ldloc_0); // push loc.0
ilgen.Emit(OpCodes.Dup); // duplicate top of stack
// stack: value ; value
ilgen.Emit(OpCodes.Isinst, methodParamType); // test, pops value, and pushes either a null ref, or an instance of the type
// stack: inst|null ; value
ilgen.Emit(OpCodes.Ldnull); // push null
// stack: null ; inst|null ; value
ilgen.Emit(OpCodes.Cgt_Un); // compare what isInst returned to null - pops 2 values, and pushes 1 if greater else 0
// stack: 0|1 ; value
ilgen.Emit(OpCodes.Brtrue_S, unbox); // pops value, branches to unbox if true, ie nonzero
// stack: value
ilgen.Convert(methodParamType); // convert
// stack: value|converted
ilgen.MarkLabel(unbox);
ilgen.Emit(OpCodes.Unbox_Any, methodParamType);
}
else
{
// parameter is reference type, but input is value type
// not supported, input should always be less constrained
// (otherwise, would require boxing and converting)
if (inputType.IsValueType)
throw new NotSupportedException("ValueType boxing.");
// both input and parameter are reference types
// cast the input to the parameter type
ilgen.Emit(OpCodes.Castclass, methodParamType);
}
}
//private static T ToT(object o)
//{
// return o is T t ? t : (T) System.Convert.ChangeType(o, typeof(T));
//}
private static MethodInfo _convertMethod;
private static MethodInfo _getTypeFromHandle;
private static void Convert(this ILGenerator ilgen, Type type)
{
if (_getTypeFromHandle == null)
_getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(RuntimeTypeHandle) }, null);
if (_convertMethod == null)
_convertMethod = typeof(Convert).GetMethod("ChangeType", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(object), typeof(Type) }, null);
ilgen.Emit(OpCodes.Ldtoken, type);
ilgen.CallMethod(_getTypeFromHandle);
ilgen.CallMethod(_convertMethod);
}
// emits adapter code before OpCodes.Ret
// outputType is the lambda output type
// methodReturnedType is the actual type returned by the actual method
// adding code to do methodReturnedType -> outputType
// valueType -> valueType : not supported ('cos, why?)
// valueType -> !valueType : box
// !valueType -> valueType : not supported ('cos, why?)
// !valueType -> !valueType : implicit cast (could throw)
private static void EmitOutputAdapter(ILGenerator ilgen, Type outputType, Type methodReturnedType)
{
if (outputType == methodReturnedType) return;
// note: the only important thing to support here, is returning a specific type
// as an object, when emitting the method as a Func<..., object> - anything else
// is pointless really - so we box value types, and ensure that non value types
// can be assigned
if (methodReturnedType.IsValueType)
{
if (outputType.IsValueType)
{
// both returned and output are value types
// not supported, use proper output
// (otherwise, would require converting)
throw new NotSupportedException("ValueTypes conversion.");
}
// returned is value type, but output is reference type
// box the returned value
ilgen.Emit(OpCodes.Box, methodReturnedType);
}
else
{
// returned is reference type, but output is value type
// not supported, output should always be less constrained
// (otherwise, would require boxing and converting)
if (outputType.IsValueType)
throw new NotSupportedException("ValueType boxing.");
// both output and returned are reference types
// as long as returned can be assigned to output, good
if (!outputType.IsAssignableFrom(methodReturnedType))
throw new NotSupportedException("Invalid cast.");
}
}
private static void ThrowInvalidLambda(string methodName, Type returned, Type[] args)
{
throw new ArgumentException($"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable) args)}):{returned}.", nameof(TLambda));
}
private static void CallMethod(this ILGenerator ilgen, MethodInfo method)
{
var virt = !method.IsStatic && (method.IsVirtual || !method.IsFinal);
ilgen.Emit(virt ? OpCodes.Callvirt : OpCodes.Call, method);
}
private static void Return(this ILGenerator ilgen)
{
ilgen.Emit(OpCodes.Ret);
}
#endregion
}
}