Wednesday, May 14, 2008

Constructor Arguments on Generic Type Parameters

This is something I've wanted on Generics since they first introduced them. It didn't come in version 3.0 or 3.5; however because I'm writing my own Objectified Intermediate Language with the goal of translating the objects into Common Intermediate Language, I can add features similarly to how C#'s compiler adds support for lambda expressions, anonymous methods, closures, and so on.

I decided to write this post after talking to Peli about this and confusing the hell out of him by not defining the problem/solution in full. I even provided a bad example of how such functionality would benefit a coder.

Let's start out with what this would mean. The primary reason you would even want this is if you wanted to create instances of some arbitrary type not explicitly defined in your code. That's what the original new() came in for. The problem with this is not all code elements are properly initializable through a zero-parameter constructor additionally they don't expose a zero-parameter constructor for this reason.

I've written a small usage example, with background code, to illustrate how the functionality will work. Using the standard 'Widget' sample, it illustrates using a list that contains multiple widget providers. Basically allowing you to define a list that is restricted to a base type, also allowing you to arbitrarily instantiate a widget by using its System.Type using much quicker instantiation methods than using a ConstructorInfo instance's Invoke method.

This would be useful for cases where you have an extensibility model that handles things through similar providers and has a need for type-parameter constructors with arguments. If, for instance, you had a project infrastructure and you allowed add-ins to define projects, and the elements that define them, through attributes or other means. You'd need a model that would enable construction of the project and its elements.

Sure you could argue that you could hard-code the specifics based upon the type requested on the provider, but that misses the point entirely.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WidgetWorks
{
class Program
{
internal static void Main()
{
Type[] widgetTypes = new Type[] { typeof(MyWidget), typeof(YourWidget), typeof(TheirWidget) };
WidgetList k = new WidgetList(new WidgetProvider<MyWidget>(), new WidgetProvider<YourWidget>(), new WidgetProvider<TheirWidget>());
Random u = new Random();
for (int i = 0; i < 6; i++)
{
int l = u.Next(1, 4);
k.Add(string.Format("Test Widget {0}", i + 1), widgetTypes[l - 1]);
}
foreach (var w in k)
{
Console.Write("{0}\n\t", w.Name);
w.DoSomething();
Console.WriteLine();
}

Console.ReadKey(true);
}
}
}
As you can see it's pretty straightforward; however, as before it doesn't utilize the instance method ConstructorInfo.Invoke. The idea is the expansion of new constructors with parameters would be like so:
    public class WidgetProvider<[GenericParamCtorSignatures(typeof(WidgetProvider<>.TCtorData))] T> :
IWidgetProvider
where T :
Widget/*,
new(string)
*/
{

#region Generic Type Constructor Code

#region T

private delegate T CreateWidget1(string name);
private static RuntimeMethodHandle _widgetCtor1Ref;
private static CreateWidget1 _widgetCtor1;

private static CreateWidget1 WidgetCtor1
{
get
{
if (_widgetCtor1 == null)
{
_widgetCtor1 = ((ConstructorInfo)(MethodBase.GetMethodFromHandle(_widgetCtor1Ref, typeof(T).TypeHandle))).BuildOptimizedConstructorDelegateEx<CreateWidget1>(typeof(CreateWidget1).Module);
_widgetCtor1Ref = default(RuntimeMethodHandle);
}
return _widgetCtor1;
}
}

[StructLayout(LayoutKind.Sequential, Size = 0)]
private struct TCtorData
{
public TCtorData(string name) { }
}

#endregion

private static void VerifyGenericParamConstructors()
{
Type t = typeof(T);
ConstructorInfo[] tCtors = t.GetConstructors();
ConstructorInfo widgetCtor1 = tCtors.FindConstructor(typeof(string));
if (widgetCtor1 == null)
throw new GenericParamCtorFailureException("T", t, typeof(string));
_widgetCtor1Ref = widgetCtor1.MethodHandle;
}
#endregion

static WidgetProvider()
{
VerifyGenericParamConstructors();
}

#region IWidgetProvider Members

public Widget GetWidget(string name)
{
return WidgetCtor1(name);
}

public Type WidgetType
{
get { return typeof(T); }
}

#endregion

}


If there is no static method it will add a simplistic one that invokes a VerifyGenericParamConstructors. Since it's not a CLR implementation, I'll have to make libraries expressly wanting this functionality rely on a small library that emits quick constructor invoke methods. It handles the quick invoke through the following code:
public static partial class LanguageMetaHelper
{
#region Type Conversion Info

private static Dictionary<TypeCode, Dictionary<TypeCode, bool>> conversionInfo = GetConversionInfo();
private static Dictionary<TypeCode, Dictionary<TypeCode, bool>> GetConversionInfo()
{
Dictionary<TypeCode, Dictionary<TypeCode, bool>> conversionInfo = new Dictionary<TypeCode, Dictionary<TypeCode, bool>>();
TypeCode[] supportedTypeCodes = new TypeCode[] { TypeCode.Byte, TypeCode.SByte, TypeCode.Single, TypeCode.Double, TypeCode.Char, TypeCode.UInt16, TypeCode.UInt32, TypeCode.UInt64, TypeCode.Int16, TypeCode.Int32, TypeCode.Int64 };
foreach (TypeCode tc in supportedTypeCodes)
{
Dictionary<TypeCode, bool> current = new Dictionary<TypeCode, bool>();
switch (tc)
{
case TypeCode.Char:
current[TypeCode.UInt16] = true;
current[TypeCode.UInt32] = true;
current[TypeCode.UInt64] = true;
current[TypeCode.Int32] = true;
current[TypeCode.Int64] = true;
current[TypeCode.Single] = true;
current[TypeCode.Double] = true;
break;
case TypeCode.Byte:
current[TypeCode.Char] = true;
current[TypeCode.UInt16] = true;
current[TypeCode.UInt32] = true;
current[TypeCode.UInt64] = true;
goto case TypeCode.SByte;
case TypeCode.SByte:
current[TypeCode.Int16] = true;
current[TypeCode.Int32] = true;
current[TypeCode.Int64] = true;
current[TypeCode.Single] = true;
current[TypeCode.Double] = true;
break;
case TypeCode.UInt16:
current[TypeCode.UInt32] = true;
current[TypeCode.UInt64] = true;
goto case TypeCode.Int16;
case TypeCode.Int16:
current[TypeCode.Int32] = true;
current[TypeCode.Int64] = true;
current[TypeCode.Single] = true;
current[TypeCode.Double] = true;
break;
case TypeCode.UInt32:
current[TypeCode.UInt64] = true;
goto case TypeCode.Int32;
case TypeCode.Int32:
current[TypeCode.Int64] = true;
current[TypeCode.Single] = true;
current[TypeCode.Double] = true;
break;
case TypeCode.UInt64:
case TypeCode.Int64:
current[TypeCode.Single] = true;
current[TypeCode.Double] = true;
break;
case TypeCode.Single:
current[TypeCode.Double] = true;
break;
}
conversionInfo[tc] = current;
}
return conversionInfo;
}
/// <summary>
/// Checks to see if you can go <paramref name="from"/> one type <paramref name="to"/> another.
/// </summary>
/// <param name="from">The type to check conversion of.</param>
/// <param name="to">The type to see if <paramref name="from"/> can go to.</param>
/// <returns>True if <paramref name="from"/> can be cast/converted <paramref name="to"/>; otherwise false.</returns>
public static bool CanConvertFrom(this Type from, Type to)
{
TypeCode fromTC = Type.GetTypeCode(from);
TypeCode toTC = Type.GetTypeCode(to);
try
{
if (fromTC != toTC)
return conversionInfo[fromTC][toTC];
else if (fromTC == TypeCode.Object)
return (to.IsAssignableFrom(from));
else
return true;
}
catch
{
return false;
}
}

#endregion

#region CtorBinding
public static ConstructorInfo FindConstructor(this ConstructorInfo[] list, params Type[] binding)
{
if (list == null)
throw new ArgumentNullException("list");
if (binding == null)
throw new ArgumentNullException("binding");
var match = new bool[list.Length];
var deviations = new Dictionary<ConstructorInfo, int>();
for (int i = 0; i < list.Length; i++)
{
var current = list[i];
var paramsInfo = current.GetParameters();
if (paramsInfo.Length != binding.Length)
continue;
match[i] = true;
for (int j = 0; j < paramsInfo.Length; j++)
{
if (paramsInfo[j].ParameterType != binding[j])
if (paramsInfo[j].ParameterType.CanConvertFrom(binding[j]))
if (deviations.ContainsKey(list[i]))
deviations[list[i]]++;
else
deviations.Add(list[i], 1);
else
{
match[i] = false;
break;
}
}
}
int index1 = 0;
try
{
return (from constructor in list
where match[index1++]
orderby deviations.ContainsKey(constructor) ? deviations[constructor] : 0
select constructor).First();
}
catch (InvalidOperationException)
{
return null;
}
}
#endregion

#region BuildOptimizedConstructorDelegate
#region Helpers
private delegate void FuncV<T>(T arg);

private static IEnumerable<TCallResult> OnAll<TItem, TCallResult>(this IEnumerable<TItem> e, Func<TItem, TCallResult> f)
{
foreach (TItem t in e)
yield return f(t);
yield break;
}

private static void OnAll<TItem>(this IEnumerable<TItem> e, FuncV<TItem> f)
{
foreach (TItem t in e)
f(t);
}

private static string GetStringFormSignature(ParameterInfo[] parameters)
{
bool firstMember = true;
StringBuilder sb = new StringBuilder();
foreach (ParameterInfo paramInfo in parameters)
{
if (firstMember)
firstMember = false;
else
sb.Append(", ");
sb.Append(paramInfo.ParameterType.FullName == null ? paramInfo.ParameterType.Name : paramInfo.ParameterType.FullName);
sb.Append(" ");
sb.Append(paramInfo.Name);
}
return sb.ToString();
}
#endregion

/// <summary>
/// Returns an optimized delegate which invokes a constructor described through <paramref name="ctor"/>.
/// </summary>
/// <typeparam name="T">The type of object created by the constructor.</typeparam>
/// <param name="ctor">The constructor to build the dynamic delegate off of.</param>
/// <returns>A new <see cref="CreateObjectInvoke{T}"/> which wraps around the
/// <paramref name="ctor"/> provided.</returns>
public static T BuildOptimizedConstructorDelegateEx<T>(this ConstructorInfo ctor, Module m)
{
Type u = typeof(T);
if (!u.IsSubclassOf(typeof(Delegate)))
throw new ArgumentException("T");
MethodInfo delegateInvoke = u.GetMethod("Invoke");
Type[] delegateTypes = delegateInvoke.GetParameters().OnAll(param => param.ParameterType).ToArray();
ParameterInfo[] ctorParameters = ctor.GetParameters();
ILGenerator interLangGenerator = null;
if (delegateTypes.Length != ctorParameters.Length)
throw new ArgumentException("ctor");
DynamicMethod optimizedCtor = new DynamicMethod(string.Format(".ctor@{0}({1})", ctor.DeclaringType.Name, GetStringFormSignature(ctorParameters)), delegateInvoke.ReturnType, delegateTypes, m);
interLangGenerator = optimizedCtor.GetILGenerator();
List<LocalBuilder> paramLocals = new List<LocalBuilder>();

int argIndex = 0;
ctorParameters.OnAll(parameter =>
{
if (parameter.IsOut || parameter.ParameterType.IsByRef)
{
if (argIndex < 128)
{
interLangGenerator.Emit(OpCodes.Ldarga_S, argIndex);
}
else
interLangGenerator.Emit(OpCodes.Ldarga, argIndex);
}
else
{
switch (argIndex)
{
case 0:
interLangGenerator.Emit(OpCodes.Ldarg_0, argIndex);
break;
case 1:
interLangGenerator.Emit(OpCodes.Ldarg_1, argIndex);
break;
case 2:
interLangGenerator.Emit(OpCodes.Ldarg_2, argIndex);
break;
case 3:
interLangGenerator.Emit(OpCodes.Ldarg_3, argIndex);
break;
default:
if (argIndex < 128)
interLangGenerator.Emit(OpCodes.Ldarg_S, argIndex);
else
interLangGenerator.Emit(OpCodes.Ldarg, argIndex);
break;
}
}
argIndex++;
});

interLangGenerator.Emit(OpCodes.Newobj, ctor);
if (ctor.DeclaringType.IsValueType)
interLangGenerator.Emit(OpCodes.Box, ctor.DeclaringType);

interLangGenerator.Emit(OpCodes.Ret);
return (T)(object)optimizedCtor.CreateDelegate(typeof(T));
}
#endregion

internal static void Init()
{
}
}


Simply speaking, it merely alters the static constructor. This works because each closed generic type is a new type and thus calls the static constructor for each unique set of type parameters. Later on I'll further the functionality for obtaining the constraint information, for parametered constructors, on type arguments. That's what the placeholder struct TCtorData is for, and why there's a generic attribute pointing to it for. Basically it'll be used as a data source for the constraint information, since there is no built in functionality for it.

Download the WidgetWorks code

No comments: