Files
Umbraco-CMS/src/Umbraco.Core/ReflectionUtilities.cs

644 lines
34 KiB
C#
Raw Normal View History

2017-09-25 12:58:54 +02:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
2017-09-29 15:50:30 +02:00
using Umbraco.Core.Exceptions;
2017-09-25 12:58:54 +02:00
namespace Umbraco.Core
{
/// <summary>
/// Provides utilities to simplify reflection.
/// </summary>
2017-09-29 15:50:30 +02:00
/// <remarks>
/// <para>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
/// </para>
/// <para>Supports emitting constructors, instance and static methods, instance property getters and
/// setters. Does not support static properties yet.</para>
/// </remarks>
2018-02-02 19:43:03 +01:00
public static partial class ReflectionUtilities
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
#region Properties
2017-09-29 15:50:30 +02:00
/// <summary>
/// Emits a property getter.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="propertyName">The name of the property.</param>
/// <param name="mustExist">A value indicating whether the property and its getter must exist.</param>
/// <returns>A property getter function. If <paramref name="mustExist"/> is <c>false</c>, returns null when the property or its getter does not exist.</returns>
/// <exception cref="ArgumentNullOrEmptyException">Occurs when <paramref name="propertyName"/> is null or empty.</exception>
/// <exception cref="InvalidOperationException">Occurs when the property or its getter does not exist.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TValue"/> does not match the type of the property.</exception>
public static Func<TDeclaring, TValue> EmitPropertyGetter<TDeclaring, TValue>(string propertyName, bool mustExist = true)
{
2018-02-02 19:43:03 +01:00
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName));
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
if (property?.GetMethod != null)
return EmitMethod<Func<TDeclaring, TValue>>(property.GetMethod);
2017-09-27 21:16:09 +02:00
2018-02-02 19:43:03 +01:00
if (!mustExist)
return default;
throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}.");
2017-09-29 15:50:30 +02:00
}
/// <summary>
/// Emits a property setter.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="propertyName">The name of the property.</param>
/// <param name="mustExist">A value indicating whether the property and its setter must exist.</param>
/// <returns>A property setter function. If <paramref name="mustExist"/> is <c>false</c>, returns null when the property or its setter does not exist.</returns>
/// <exception cref="ArgumentNullOrEmptyException">Occurs when <paramref name="propertyName"/> is null or empty.</exception>
/// <exception cref="InvalidOperationException">Occurs when the property or its setter does not exist.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TValue"/> does not match the type of the property.</exception>
public static Action<TDeclaring, TValue> EmitPropertySetter<TDeclaring, TValue>(string propertyName, bool mustExist = true)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName));
2017-09-29 15:50:30 +02:00
var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
2018-02-02 19:43:03 +01:00
if (property?.SetMethod != null)
return EmitMethod<Action<TDeclaring, TValue>>(property.SetMethod);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
if (!mustExist)
return default;
throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}.");
2017-09-25 12:58:54 +02:00
}
2017-09-29 15:50:30 +02:00
/// <summary>
/// Emits a property getter and setter.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="propertyName">The name of the property.</param>
/// <param name="mustExist">A value indicating whether the property and its getter and setter must exist.</param>
/// <returns>A property getter and setter functions. If <paramref name="mustExist"/> is <c>false</c>, returns null when the property or its getter or setter does not exist.</returns>
/// <exception cref="ArgumentNullOrEmptyException">Occurs when <paramref name="propertyName"/> is null or empty.</exception>
/// <exception cref="InvalidOperationException">Occurs when the property or its getter or setter does not exist.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TValue"/> does not match the type of the property.</exception>
public static (Func<TDeclaring, TValue>, Action<TDeclaring, TValue>) EmitPropertyGetterAndSetter<TDeclaring, TValue>(string propertyName, bool mustExist = true)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName));
2017-09-29 15:50:30 +02:00
var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
2018-02-02 19:43:03 +01:00
if (property?.GetMethod != null && property.SetMethod != null)
return (
EmitMethod<Func<TDeclaring, TValue>>(property.GetMethod),
EmitMethod<Action<TDeclaring, TValue>>(property.SetMethod));
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
if (!mustExist)
return default;
throw new InvalidOperationException($"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}.");
2017-09-25 12:58:54 +02:00
}
2017-09-29 15:50:30 +02:00
/// <summary>
/// Emits a property getter.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="propertyInfo">The property info.</param>
/// <returns>A property getter function.</returns>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="propertyInfo"/> is null.</exception>
/// <exception cref="ArgumentException">Occurs when the property has no getter.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TValue"/> does not match the type of the property.</exception>
public static Func<TDeclaring, TValue> EmitPropertyGetter<TDeclaring, TValue>(PropertyInfo propertyInfo)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo));
2017-09-29 15:50:30 +02:00
if (propertyInfo.GetMethod == null)
throw new ArgumentException("Property has no getter.", nameof(propertyInfo));
2018-02-02 19:43:03 +01:00
return EmitMethod<Func<TDeclaring, TValue>>(propertyInfo.GetMethod);
2017-09-25 12:58:54 +02:00
}
2017-09-29 15:50:30 +02:00
/// <summary>
/// Emits a property setter.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="propertyInfo">The property info.</param>
/// <returns>A property setter function.</returns>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="propertyInfo"/> is null.</exception>
/// <exception cref="ArgumentException">Occurs when the property has no setter.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TValue"/> does not match the type of the property.</exception>
public static Action<TDeclaring, TValue> EmitPropertySetter<TDeclaring, TValue>(PropertyInfo propertyInfo)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo));
2017-09-29 15:50:30 +02:00
if (propertyInfo.SetMethod == null)
throw new ArgumentException("Property has no setter.", nameof(propertyInfo));
return EmitMethod<Action<TDeclaring, TValue>>(propertyInfo.SetMethod);
}
/// <summary>
/// Emits a property getter and setter.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="propertyInfo">The property info.</param>
/// <returns>A property getter and setter functions.</returns>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="propertyInfo"/> is null.</exception>
/// <exception cref="ArgumentException">Occurs when the property has no getter or no setter.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TValue"/> does not match the type of the property.</exception>
public static (Func<TDeclaring, TValue>, Action<TDeclaring, TValue>) EmitPropertyGetterAndSetter<TDeclaring, TValue>(PropertyInfo propertyInfo)
{
2018-02-02 19:43:03 +01:00
if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo));
2017-09-29 15:50:30 +02:00
if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null)
throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo));
return (
EmitMethod<Func<TDeclaring, TValue>>(propertyInfo.GetMethod),
EmitMethod<Action<TDeclaring, TValue>>(propertyInfo.SetMethod));
}
2018-02-02 19:43:03 +01:00
/// <summary>
/// Emits a property setter.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="propertyInfo">The property info.</param>
/// <returns>A property setter function.</returns>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="propertyInfo"/> is null.</exception>
/// <exception cref="ArgumentException">Occurs when the property has no setter.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TValue"/> does not match the type of the property.</exception>
public static Action<TDeclaring, TValue> EmitPropertySetterUnsafe<TDeclaring, TValue>(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<Action<TDeclaring, TValue>>(propertyInfo.SetMethod);
}
#endregion
#region Constructors
2017-09-29 15:50:30 +02:00
/// <summary>
/// Emits a constructor.
/// </summary>
/// <typeparam name="TLambda">A lambda representing the constructor.</typeparam>
/// <param name="mustExist">A value indicating whether the constructor must exist.</param>
/// <param name="declaring">The optional type of the class to construct.</param>
/// <returns>A constructor function. If <paramref name="mustExist"/> is <c>false</c>, returns null when the constructor does not exist.</returns>
/// <remarks>
/// <para>When <paramref name="declaring"/> is not specified, it is the type returned by <typeparamref name="TLambda"/>.</para>
/// <para>The constructor arguments are determined by <typeparamref name="TLambda"/> generic arguments.</para>
/// <para>The type returned by <typeparamref name="TLambda"/> does not need to be exactly <paramref name="declaring"/>,
/// when e.g. that type is not known at compile time, but it has to be a parent type (eg an interface, or <c>object</c>).</para>
/// </remarks>
/// <exception cref="InvalidOperationException">Occurs when the constructor does not exist and <paramref name="mustExist"/> is <c>true</c>.</exception>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TLambda"/> is not a Func or when <paramref name="declaring"/>
/// is specified and does not match the function's returned type.</exception>
public static TLambda EmitCtor<TLambda>(bool mustExist = true, Type declaring = null)
{
2018-02-02 19:43:03 +01:00
(_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda<TLambda>(true, true);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
// 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));
2017-09-25 12:58:54 +02:00
// get the constructor infos
2018-02-02 19:43:03 +01:00
var ctor = declaring.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null);
2017-09-25 12:58:54 +02:00
if (ctor == null)
2017-09-27 21:16:09 +02:00
{
2017-09-29 15:50:30 +02:00
if (!mustExist) return default;
2018-02-02 19:43:03 +01:00
throw new InvalidOperationException($"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable<Type>) lambdaParameters)}).");
2017-09-27 21:16:09 +02:00
}
2017-09-25 12:58:54 +02:00
2017-09-29 15:50:30 +02:00
// emit
2018-02-02 19:43:03 +01:00
return EmitCtorSafe<TLambda>(lambdaParameters, lambdaReturned, ctor);
2017-09-25 12:58:54 +02:00
}
2017-09-29 15:50:30 +02:00
/// <summary>
/// Emits a constructor.
/// </summary>
/// <typeparam name="TLambda">A lambda representing the constructor.</typeparam>
/// <param name="ctor">The constructor info.</param>
/// <returns>A constructor function.</returns>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TLambda"/> is not a Func or when its generic
/// arguments do not match those of <paramref name="ctor"/>.</exception>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="ctor"/> is null.</exception>
public static TLambda EmitCtor<TLambda>(ConstructorInfo ctor)
2017-09-26 14:57:50 +02:00
{
2018-02-02 19:43:03 +01:00
if (ctor == null) throw new ArgumentNullException(nameof(ctor));
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
(_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda<TLambda>(true, true);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
return EmitCtorSafe<TLambda>(lambdaParameters, lambdaReturned, ctor);
}
private static TLambda EmitCtorSafe<TLambda>(Type[] lambdaParameters, Type returned, ConstructorInfo ctor)
{
// get type and args
var ctorDeclaring = ctor.DeclaringType;
var ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray();
2017-09-29 15:50:30 +02:00
// validate arguments
2018-02-02 19:43:03 +01:00
if (lambdaParameters.Length != ctorParameters.Length)
ThrowInvalidLambda<TLambda>("ctor", ctorDeclaring, ctorParameters);
for (var i = 0; i < lambdaParameters.Length; i++)
2018-02-09 18:22:59 +01:00
if (lambdaParameters[i] != ctorParameters[i]) // note: relax the constraint with IsAssignableFrom?
2018-02-02 19:43:03 +01:00
ThrowInvalidLambda<TLambda>("ctor", ctorDeclaring, ctorParameters);
if (!returned.IsAssignableFrom(ctorDeclaring))
ThrowInvalidLambda<TLambda>("ctor", ctorDeclaring, ctorParameters);
2017-09-26 14:57:50 +02:00
2017-09-29 15:50:30 +02:00
// emit
2018-02-02 19:43:03 +01:00
return EmitCtor<TLambda>(ctorDeclaring, ctorParameters, ctor);
2017-09-29 15:51:33 +02:00
}
/// <summary>
/// Emits a constructor.
/// </summary>
/// <typeparam name="TLambda">A lambda representing the constructor.</typeparam>
/// <param name="ctor">The constructor info.</param>
/// <returns>A constructor function.</returns>
/// <remarks>
/// <para>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.</para>
/// </remarks>
/// <exception cref="ArgumentException">Occurs when <typeparamref name="TLambda"/> is not a Func or when its generic
/// arguments do not match those of <paramref name="ctor"/>.</exception>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="ctor"/> is null.</exception>
public static TLambda EmitCtorUnsafe<TLambda>(ConstructorInfo ctor)
{
2018-02-02 19:43:03 +01:00
if (ctor == null) throw new ArgumentNullException(nameof(ctor));
2017-09-29 15:51:33 +02:00
2018-02-02 19:43:03 +01:00
(_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda<TLambda>(true, true);
2017-09-29 15:51:33 +02:00
2018-02-02 19:43:03 +01:00
// emit - unsafe - use lambda's args and assume they are correct
return EmitCtor<TLambda>(lambdaReturned, lambdaParameters, ctor);
}
2017-09-29 15:51:33 +02:00
2018-02-02 19:43:03 +01:00
private static TLambda EmitCtor<TLambda>(Type declaring, Type[] lambdaParameters, ConstructorInfo ctor)
{
// gets the method argument types
var ctorParameters = GetParameters(ctor);
2017-09-29 15:51:33 +02:00
// emit
2018-02-02 19:43:03 +01:00
(var dm, var ilgen) = CreateIlGenerator(ctor.DeclaringType?.Module, lambdaParameters, declaring);
EmitLdargs(ilgen, lambdaParameters, ctorParameters);
2017-09-29 15:51:33 +02:00
ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects
ilgen.Emit(OpCodes.Ret);
2018-02-02 19:43:03 +01:00
2017-09-29 15:51:33 +02:00
return (TLambda) (object) dm.CreateDelegate(typeof(TLambda));
2017-09-27 21:16:09 +02:00
}
2018-02-02 19:43:03 +01:00
#endregion
#region Methods
2017-09-27 21:16:09 +02:00
2018-02-02 19:43:03 +01:00
/// <summary>
/// Emits a static method.
/// </summary>
/// <typeparam name="TDeclaring">The declaring type.</typeparam>
/// <typeparam name="TLambda">A lambda representing the method.</typeparam>
/// <param name="methodName">The name of the method.</param>
/// <param name="mustExist">A value indicating whether the constructor must exist.</param>
/// <returns>The method. If <paramref name="mustExist"/> is <c>false</c>, returns null when the method does not exist.</returns>
/// <remarks>
/// <para>The method arguments are determined by <typeparamref name="TLambda"/> generic arguments.</para>
/// </remarks>
/// <exception cref="ArgumentNullOrEmptyException">Occurs when <paramref name="methodName"/> is null or empty.</exception>
/// <exception cref="InvalidOperationException">Occurs when no proper method with name <paramref name="methodName"/> could be found.</exception>
/// <exception cref="ArgumentException">Occurs when Occurs when <typeparamref name="TLambda"/> does not match the method signature.</exception>
public static TLambda EmitMethod<TDeclaring, TLambda>(string methodName, bool mustExist = true)
2017-09-27 21:16:09 +02:00
{
2018-02-02 19:43:03 +01:00
return EmitMethod<TLambda>(typeof(TDeclaring), methodName, mustExist);
2017-09-27 21:16:09 +02:00
}
2018-02-02 19:43:03 +01:00
/// <summary>
/// Emits a static method.
/// </summary>
/// <typeparam name="TLambda">A lambda representing the method.</typeparam>
/// <param name="declaring">The declaring type.</param>
/// <param name="methodName">The name of the method.</param>
/// <param name="mustExist">A value indicating whether the constructor must exist.</param>
/// <returns>The method. If <paramref name="mustExist"/> is <c>false</c>, returns null when the method does not exist.</returns>
/// <remarks>
/// <para>The method arguments are determined by <typeparamref name="TLambda"/> generic arguments.</para>
/// </remarks>
/// <exception cref="ArgumentNullOrEmptyException">Occurs when <paramref name="methodName"/> is null or empty.</exception>
/// <exception cref="InvalidOperationException">Occurs when no proper method with name <paramref name="methodName"/> could be found.</exception>
/// <exception cref="ArgumentException">Occurs when Occurs when <typeparamref name="TLambda"/> does not match the method signature.</exception>
public static TLambda EmitMethod<TLambda>(Type declaring, string methodName, bool mustExist = true)
2017-09-27 21:16:09 +02:00
{
2018-02-02 19:43:03 +01:00
if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentNullOrEmptyException(nameof(methodName));
2017-09-27 21:16:09 +02:00
2018-02-02 19:43:03 +01:00
(_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda<TLambda>(true, out var isFunction);
2017-09-27 21:16:09 +02:00
2018-02-02 19:43:03 +01:00
// 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<Type>) lambdaParameters)}).");
}
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
// emit
return EmitMethod<TLambda>(lambdaReturned, lambdaParameters, method);
2017-09-26 14:57:50 +02:00
}
2017-09-29 15:50:30 +02:00
/// <summary>
/// Emits a method.
/// </summary>
/// <typeparam name="TLambda">A lambda representing the method.</typeparam>
/// <param name="method">The method info.</param>
/// <returns>The method.</returns>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">Occurs when Occurs when <typeparamref name="TLambda"/> does not match the method signature.</exception>
public static TLambda EmitMethod<TLambda>(MethodInfo method)
2017-09-27 21:16:09 +02:00
{
2018-02-02 19:43:03 +01:00
if (method == null) throw new ArgumentNullException(nameof(method));
2017-09-27 21:16:09 +02:00
2017-09-29 15:50:30 +02:00
// get type and args
2018-02-02 19:43:03 +01:00
var methodDeclaring = method.DeclaringType;
var methodReturned = method.ReturnType;
var methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray();
2017-09-29 15:50:30 +02:00
var isStatic = method.IsStatic;
2018-02-02 19:43:03 +01:00
(var lambdaDeclaring, var lambdaParameters, var lambdaReturned) = AnalyzeLambda<TLambda>(isStatic, out var isFunction);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
// if not static, then the first lambda arg must be the method declaring type
if (!isStatic && (methodDeclaring == null || !methodDeclaring.IsAssignableFrom(lambdaDeclaring)))
ThrowInvalidLambda<TLambda>(method.Name, methodReturned, methodParameters);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
if (methodParameters.Length != lambdaParameters.Length)
ThrowInvalidLambda<TLambda>(method.Name, methodReturned, methodParameters);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
for (var i = 0; i < methodParameters.Length; i++)
if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i]))
ThrowInvalidLambda<TLambda>(method.Name, methodReturned, methodParameters);
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
// if it's a function then the last lambda arg must match the method returned type
if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned))
ThrowInvalidLambda<TLambda>(method.Name, methodReturned, methodParameters);
2017-09-29 15:50:30 +02:00
// emit
2018-02-02 19:43:03 +01:00
return EmitMethod<TLambda>(lambdaReturned, lambdaParameters, method);
2017-09-29 15:50:30 +02:00
}
/// <summary>
2018-02-02 19:43:03 +01:00
/// Emits a method.
2017-09-29 15:50:30 +02:00
/// </summary>
/// <typeparam name="TLambda">A lambda representing the method.</typeparam>
2018-02-02 19:43:03 +01:00
/// <param name="method">The method info.</param>
/// <returns>The method.</returns>
/// <exception cref="ArgumentNullException">Occurs when <paramref name="method"/> is null.</exception>
2017-09-29 15:50:30 +02:00
/// <exception cref="ArgumentException">Occurs when Occurs when <typeparamref name="TLambda"/> does not match the method signature.</exception>
2018-02-02 19:43:03 +01:00
public static TLambda EmitMethodUnsafe<TLambda>(MethodInfo method)
2017-09-29 15:50:30 +02:00
{
2018-02-02 19:43:03 +01:00
if (method == null) throw new ArgumentNullException(nameof(method));
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
var isStatic = method.IsStatic;
(_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda<TLambda>(isStatic, out _);
2017-09-27 21:16:09 +02:00
2018-02-02 19:43:03 +01:00
// emit - unsafe - use lambda's args and assume they are correct
return EmitMethod<TLambda>(lambdaReturned, lambdaParameters, method);
2017-09-29 15:50:30 +02:00
}
/// <summary>
2018-02-02 19:43:03 +01:00
/// Emits an instance method.
2017-09-29 15:50:30 +02:00
/// </summary>
/// <typeparam name="TLambda">A lambda representing the method.</typeparam>
/// <param name="methodName">The name of the method.</param>
/// <param name="mustExist">A value indicating whether the constructor must exist.</param>
/// <returns>The method. If <paramref name="mustExist"/> is <c>false</c>, returns null when the method does not exist.</returns>
/// <remarks>
/// <para>The method arguments are determined by <typeparamref name="TLambda"/> generic arguments.</para>
/// </remarks>
/// <exception cref="ArgumentNullOrEmptyException">Occurs when <paramref name="methodName"/> is null or empty.</exception>
/// <exception cref="InvalidOperationException">Occurs when no proper method with name <paramref name="methodName"/> could be found.</exception>
/// <exception cref="ArgumentException">Occurs when Occurs when <typeparamref name="TLambda"/> does not match the method signature.</exception>
2018-02-02 19:43:03 +01:00
public static TLambda EmitMethod<TLambda>(string methodName, bool mustExist = true)
2017-09-29 15:50:30 +02:00
{
if (string.IsNullOrWhiteSpace(methodName))
throw new ArgumentNullOrEmptyException(nameof(methodName));
// validate lambda type
2018-02-02 19:43:03 +01:00
(var declaring, var lambdaParameters, var lambdaReturned) = AnalyzeLambda<TLambda>(false, out var isFunction);
2017-09-29 15:50:30 +02:00
// get the method infos
2018-02-02 19:43:03 +01:00
var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null);
if (method == null || isFunction && method.ReturnType != lambdaReturned)
2017-09-27 21:16:09 +02:00
{
2017-09-29 15:50:30 +02:00
if (!mustExist) return default;
2018-02-02 19:43:03 +01:00
throw new InvalidOperationException($"Could not find method {declaring}.{methodName}({string.Join(", ", (IEnumerable<Type>) lambdaParameters)}).");
2017-09-27 21:16:09 +02:00
}
2017-09-29 15:50:30 +02:00
// emit
2018-02-02 19:43:03 +01:00
return EmitMethod<TLambda>(lambdaReturned, lambdaParameters, method);
2017-09-27 21:16:09 +02:00
}
2018-02-02 19:43:03 +01:00
// lambdaReturned = the lambda returned type (can be void)
// lambdaArgTypes = the lambda argument types
private static TLambda EmitMethod<TLambda>(Type lambdaReturned, Type[] lambdaParameters, MethodInfo method)
2017-09-27 21:16:09 +02:00
{
2018-02-02 19:43:03 +01:00
// non-static methods need the declaring type as first arg
var parameters = lambdaParameters;
2017-09-29 15:50:30 +02:00
if (!method.IsStatic)
2017-09-27 21:16:09 +02:00
{
2018-02-02 19:43:03 +01:00
parameters = new Type[lambdaParameters.Length + 1];
parameters[0] = method.DeclaringType;
Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length);
2017-09-27 21:16:09 +02:00
}
2017-09-29 15:50:30 +02:00
2018-02-02 19:43:03 +01:00
// gets the method argument types
var methodArgTypes = GetParameters(method, withDeclaring: !method.IsStatic);
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
// emit IL
(var dm, var ilgen) = CreateIlGenerator(method.DeclaringType?.Module, parameters, lambdaReturned);
EmitLdargs(ilgen, parameters, methodArgTypes);
ilgen.Emit(method.IsStatic ? OpCodes.Call : OpCodes.Callvirt, method);
EmitOutputAdapter(ilgen, lambdaReturned, method.ReturnType);
ilgen.Emit(OpCodes.Ret);
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
// create
return (TLambda) (object) dm.CreateDelegate(typeof(TLambda));
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
#endregion
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
#region Utilities
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
// 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<TLambda>(bool isStatic, bool isFunction)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
var typeLambda = typeof(TLambda);
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
(var declaring, var parameters, var returned) = AnalyzeLambda<TLambda>(isStatic, out var maybeFunction);
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
if (isFunction)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (!maybeFunction)
throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda));
2017-09-25 12:58:54 +02:00
}
else
{
2018-02-02 19:43:03 +01:00
if (maybeFunction)
throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda));
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
return (declaring, parameters, returned);
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
// 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<TLambda>(bool isStatic, out bool isFunction)
2017-09-27 21:16:09 +02:00
{
2018-02-02 19:43:03 +01:00
isFunction = false;
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
var typeLambda = typeof(TLambda);
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
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));
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
return (null, Array.Empty<Type>(), typeof(void));
}
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null;
var name = genericDefinition?.FullName;
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
if (name == null)
throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda));
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
var isActionOf = name.StartsWith("System.Action`");
isFunction = name.StartsWith("System.Func`");
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
if (!isActionOf && !isFunction)
throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda));
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
var genericArgs = typeLambda.GetGenericArguments();
if (genericArgs.Length == 0)
throw new Exception("Panic: Func<> or Action<> has zero generic arguments.");
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
var i = 0;
var declaring = isStatic ? typeof(void) : genericArgs[i++];
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
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));
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
var parameters = new Type[parameterCount];
for (var j = 0; j < parameterCount; j++)
parameters[j] = genericArgs[i++];
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
var returned = isFunction ? genericArgs[i] : typeof(void);
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
return (declaring, parameters, returned);
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module module, Type[] arguments, Type returned)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (module == null) throw new ArgumentNullException(nameof(module));
var dm = new DynamicMethod(string.Empty, returned, arguments, module, true);
return (dm, dm.GetILGenerator());
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
private static Type[] GetParameters(ConstructorInfo ctor)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
var parameters = ctor.GetParameters();
var types = new Type[parameters.Length];
var i = 0;
foreach (var parameter in parameters)
types[i++] = parameter.ParameterType;
return types;
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
private static Type[] GetParameters(MethodInfo method, bool withDeclaring)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
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;
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
// emits args
private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
var ldargOpCodes = new[] { OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 };
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
if (lambdaArgTypes.Length != methodArgTypes.Length)
throw new Exception("Panic: inconsistent number of args.");
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
for (var i = 0; i < lambdaArgTypes.Length; i++)
{
if (lambdaArgTypes.Length < 5)
ilgen.Emit(ldargOpCodes[i]);
else
ilgen.Emit(OpCodes.Ldarg, i);
}
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
// emits adapter opcodes before OpCodes.Ret
private static void EmitOutputAdapter(ILGenerator ilgen, Type outputType, Type methodReturnedType)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (outputType == methodReturnedType) return;
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
if (methodReturnedType.IsValueType)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
if (outputType.IsValueType)
{
// both returned and output are value types
// not supported, use proper output
// (otherwise, would require converting)
throw new NotSupportedException("ValueTypes conversion.");
}
2017-09-25 12:58:54 +02:00
2018-02-02 19:43:03 +01:00
// 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.");
2017-09-25 12:58:54 +02:00
}
}
2018-02-02 19:43:03 +01:00
private static void ThrowInvalidLambda<TLambda>(string methodName, Type returned, Type[] args)
2017-09-25 12:58:54 +02:00
{
2018-02-02 19:43:03 +01:00
throw new ArgumentException($"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable<Type>) args)}):{returned}.", nameof(TLambda));
2017-09-25 12:58:54 +02:00
}
2018-02-02 19:43:03 +01:00
#endregion
2017-09-25 12:58:54 +02:00
}
}