Multimethods in C# revisited - MultimethodFactory.cs
/// <summary>
/// Generates multimethods corresponding to the specified method signature.
/// </summary>
/// <typeparam name="T">Type of delegate specifying method signature</typeparam>
public sealed class MultimethodFactory<T> where T : class
{
private static class Constants
{
internal const string InvokeMethodName = "Invoke";
internal const string SpecificInvokeMethodName = "__Invoke";
internal const string CreateDelegateMethodName = "CreateDelegate";
}
private sealed class FieldData
{
private readonly FieldBuilder _fieldBuilder;
private readonly object _value;
internal FieldBuilder FieldBuilder
{
get
{
return _fieldBuilder;
}
}
internal object Value
{
get
{
return _value;
}
}
internal FieldData(FieldBuilder fieldBuilder, object value)
{
Debug.Assert(null != fieldBuilder);
Debug.Assert(null != value);
_fieldBuilder = fieldBuilder;
_value = value;
}
}
private static readonly InvocationData _invocationData;
private readonly List<object> _delegates = new List<object>();
/// <summary>
/// Type initializer.
/// </summary>
static MultimethodFactory()
{
if (!typeof(T).IsPublic || !InvocationData.TryCreate(typeof(T), out _invocationData))
throw new ArgumentException(string.Format(Properties.Resources.ArgumentExceptionMustBeValidPublicDelegateType, typeof(T).FullName), "T");
}
/// <summary>
/// Creates a new multimethod factory instance.
/// </summary>
public MultimethodFactory()
{
}
/// <summary>
/// Implements a runtime overload of the multimethod.
/// </summary>
/// <typeparam name="U">Type of delegate specifying method signature</typeparam>
/// <param name="delegate">Delegate</param>
public void Implement<U>(U @delegate) where U : class
{
InvocationData invocationData;
if (!typeof(U).IsPublic || !InvocationData.TryCreate(typeof(U), out invocationData))
throw new ArgumentException(string.Format(Properties.Resources.ArgumentExceptionMustBeValidPublicDelegateType, typeof(U).FullName), "U");
if (null == @delegate)
throw new ArgumentNullException(Properties.Resources.ArgumentNullExceptionDelegate, "delegate");
Debug.Assert(typeof(U).Equals(@delegate.GetType()));
if (!_invocationData.IsCompatibleWith(invocationData))
throw new ArgumentException(string.Format(Properties.Resources.ArgumentExceptionDelegateTypeIsIncompatible, typeof(U).FullName, typeof(T).FullName), "U");
_delegates.Add(@delegate);
}
/// <summary>
/// Creates a multimethod instance.
/// </summary>
/// <returns>Multimethod delegate</returns>
public T CreateMultimethod()
{
return CreateMultimethod(null);
}
/// <summary>
/// Creates a multimethod instance.
/// </summary>
/// <param name="debugInfo">Debug configuration</param>
/// <returns>Multimethod delegate</returns>
public T CreateMultimethod(DebugInfo debugInfo)
{
if (_delegates.Count < 1)
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionNoOverloadsDefined);
AssemblyBuilder assemblyBuilder = CreateAssemblyBuilder(debugInfo);
ModuleBuilder moduleBuilder = CreateModuleBuilder(debugInfo, assemblyBuilder);
FieldData[] fields;
Type holderType = CreateHolderClass(debugInfo, moduleBuilder, _delegates.ToArray(), _invocationData, out fields);
SaveArtifacts(debugInfo, assemblyBuilder, moduleBuilder);
object holder = CreateHolderInstance(holderType, fields);
MethodInfo createDelegateMethod = holderType.GetMethod(Constants.CreateDelegateMethodName, BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null);
if (null == createDelegateMethod)
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
return (T)createDelegateMethod.Invoke(holder, null);
}
private static void SaveArtifacts(DebugInfo debugInfo, AssemblyBuilder assemblyBuilder, ModuleBuilder moduleBuilder)
{
Debug.Assert(null != assemblyBuilder);
Debug.Assert(null != moduleBuilder);
if (null == debugInfo)
return;
// Copy the assembly to the output path.
string fileName = Path.GetFileName(debugInfo.AssemblyPath);
assemblyBuilder.Save(fileName);
File.Move(fileName, debugInfo.AssemblyPath);
// Copy the module to the output path.
File.Move(moduleBuilder.FullyQualifiedName, debugInfo.ModulePath);
// Copy the source to the output path.
string symbolSourcePath = Path.ChangeExtension(moduleBuilder.FullyQualifiedName, ".pdb");
string symbolTargetPath = Path.ChangeExtension(debugInfo.ModulePath, ".pdb");
File.Move(symbolSourcePath, symbolTargetPath);
}
private static AssemblyBuilder CreateAssemblyBuilder(DebugInfo debugInfo)
{
AssemblyName assemblyName = new AssemblyName(null == debugInfo ? "ANONYMOUS" : Path.GetFileName(debugInfo.AssemblyPath));
ConstructorInfo attributeConstructor = typeof(SecurityTransparentAttribute).GetConstructor(Type.EmptyTypes);
if (null == attributeConstructor)
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave, new CustomAttributeBuilder[] { new CustomAttributeBuilder(attributeConstructor, new object[0]) });
attributeConstructor = typeof(DebuggableAttribute).GetConstructor(new Type[] { typeof(DebuggableAttribute.DebuggingModes) });
assemblyBuilder.SetCustomAttribute(new CustomAttributeBuilder(attributeConstructor, new object[] { DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.Default }));
return assemblyBuilder;
}
private static ModuleBuilder CreateModuleBuilder(DebugInfo debugInfo, AssemblyBuilder assemblyBuilder)
{
Debug.Assert(null != assemblyBuilder);
if (null == debugInfo)
return assemblyBuilder.DefineDynamicModule("ANONYMOUS");
return assemblyBuilder.DefineDynamicModule(debugInfo.ModulePath, Path.GetFileName(debugInfo.ModulePath), true);
}
private static Type CreateHolderClass(DebugInfo debugInfo, ModuleBuilder moduleBuilder, object[] delegates, InvocationData invocationData, out FieldData[] fields)
{
Debug.Assert(null != moduleBuilder);
Debug.Assert(null != delegates && delegates.Length > 0);
Debug.Assert(null != invocationData);
bool isDebuggable = null != debugInfo;
string sourceFileName = isDebuggable ? debugInfo.SourcePath : null;
using (SourceWriter sourceWriter = new SourceWriter(isDebuggable, sourceFileName, moduleBuilder))
{
TypeBuilder typeBuilder = moduleBuilder.DefineType(ILHelpers.GenerateUniqueName("MultimethodHolderClass"), TypeAttributes.Public | TypeAttributes.Class);
fields = new FieldData[delegates.Length];
for (int i = 0; i < fields.Length; ++i)
{
// Create runtime overload of "__Invoke" and save the field name for later use.
object @delegate = delegates[i];
FieldBuilder fieldBuilder = GenerateSpecificInvokeMethod(sourceWriter, typeBuilder, @delegate.GetType());
fields[i] = new FieldData(fieldBuilder, @delegate);
}
GenerateConstructor(sourceWriter, typeBuilder, fields);
// Generate public "Invoke" method.
MethodBuilder publicInvokeMethod = GenerateInvokeMethod(sourceWriter, typeBuilder, invocationData);
// Create delegate creator method.
GenerateCreateDelegateMethod(sourceWriter, typeBuilder, typeof(T), publicInvokeMethod);
// Instantiate generated multimethod holder class.
return typeBuilder.CreateType();
}
}
private static object CreateHolderInstance(Type holderType, FieldData[] fields)
{
Debug.Assert(null != holderType);
Debug.Assert(null != fields && fields.Length > 0);
object[] args = new object[fields.Length];
for (int i = 0; i < args.Length; ++i)
args[i] = fields[i].Value;
return Activator.CreateInstance(holderType, args);
}
private static void GenerateConstructor(SourceWriter sourceWriter, TypeBuilder typeBuilder, FieldData[] fields)
{
Debug.Assert(null != sourceWriter);
Debug.Assert(null != typeBuilder);
Debug.Assert(null != fields && fields.Length > 0);
// Create array of delegate types for constructor.
Type[] parameterTypes = new Type[fields.Length];
for (int i = 0; i < parameterTypes.Length; ++i)
parameterTypes[i] = fields[i].Value.GetType();
// Default constructor for System.Object.
ConstructorInfo objectConstructor = typeof(object).GetConstructor(Type.EmptyTypes);
if (null == objectConstructor)
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, parameterTypes);
ILGenerator generator = constructorBuilder.GetILGenerator();
sourceWriter.EmitFunctionHeader(".ctor");
sourceWriter.EmitSourceLine(generator, "PUSH this");
generator.Emit(OpCodes.Ldarg_0); // Push "this" onto stack.
sourceWriter.EmitSourceLine(generator, "CALL System.Object..ctor");
generator.Emit(OpCodes.Call, objectConstructor); // Invoke System.Object..ctor.
for (int i = 1; i <= fields.Length; ++i)
{
sourceWriter.EmitSourceLine(generator, "PUSH this");
generator.Emit(OpCodes.Ldarg_0); // Push "this" onto stack.
sourceWriter.EmitSourceLine(generator, "PUSH ARG {0}", i);
ILHelpers.EmitLdarg(generator, i);
sourceWriter.EmitSourceLine(generator, "STORE FIELD {0}", fields[i - 1].FieldBuilder.Name);
generator.Emit(OpCodes.Stfld, fields[i - 1].FieldBuilder); // Store in field.
}
sourceWriter.EmitSourceLine(generator, "RETURN");
generator.Emit(OpCodes.Ret);
}
private static void GenerateCreateDelegateMethod(SourceWriter sourceWriter, TypeBuilder typeBuilder, Type delegateType, MethodBuilder publicInvokeMethod)
{
Debug.Assert(null != sourceWriter);
Debug.Assert(null != typeBuilder);
Debug.Assert(null != delegateType && typeof(Delegate).IsAssignableFrom(delegateType));
Debug.Assert(null != publicInvokeMethod);
// Get delegate's constructor.
ConstructorInfo delegateConstructor = delegateType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) });
if (null == delegateConstructor)
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
// Generate delegate factory method.
MethodBuilder methodBuilder = typeBuilder.DefineMethod(Constants.CreateDelegateMethodName, MethodAttributes.Public, delegateType, Type.EmptyTypes);
ILGenerator generator = methodBuilder.GetILGenerator();
sourceWriter.EmitFunctionHeader(Constants.CreateDelegateMethodName);
sourceWriter.EmitSourceLine(generator, "DECLARE LOCAL");
generator.DeclareLocal(delegateType);
sourceWriter.EmitSourceLine(generator, "PUSH this");
generator.Emit(OpCodes.Ldarg_0); // Push "this" onto stack.
sourceWriter.EmitSourceLine(generator, "PUSH {0}", publicInvokeMethod.Name);
generator.Emit(OpCodes.Ldftn, publicInvokeMethod); // Push reference to "__Invoke" method onto stack.
sourceWriter.EmitSourceLine(generator, "CREATE {0}", delegateType.FullName);
generator.Emit(OpCodes.Newobj, delegateConstructor); // Create instance of delegate.
sourceWriter.EmitSourceLine(generator, "STORE LOCAL");
generator.Emit(OpCodes.Stloc_0); // Store to local variable.
sourceWriter.EmitSourceLine(generator, "PUSH LOCAL");
generator.Emit(OpCodes.Ldloc_0); // Push local variable onto stack to return it.
sourceWriter.EmitSourceLine(generator, "RETURN");
generator.Emit(OpCodes.Ret); // Return;
}
private static FieldBuilder GenerateSpecificInvokeMethod(SourceWriter sourceWriter, TypeBuilder typeBuilder, Type delegateType)
{
Debug.Assert(null != typeBuilder);
Debug.Assert(null != sourceWriter);
Debug.Assert(null != delegateType && typeof(Delegate).IsAssignableFrom(delegateType));
InvocationData invocationData;
if (!InvocationData.TryCreate(delegateType, out invocationData))
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
// Generate a random name for the field.
string fieldName = ILHelpers.GenerateUniqueName("InvokerDelegate");
// Create the private delegate field.
FieldBuilder fieldBuilder = typeBuilder.DefineField(fieldName, delegateType, FieldAttributes.Private | FieldAttributes.InitOnly);
// Generate the "__Invoke" method.
MethodBuilder methodBuilder = typeBuilder.DefineMethod(
Constants.SpecificInvokeMethodName,
MethodAttributes.Public,
invocationData.InvokeMethod.ReturnType,
invocationData.GetParameterTypes());
ILGenerator generator = methodBuilder.GetILGenerator();
sourceWriter.EmitFunctionHeader(string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", Constants.SpecificInvokeMethodName, delegateType.FullName));
sourceWriter.EmitSourceLine(generator, "PUSH DELEGATE ONTO STACK");
generator.Emit(OpCodes.Ldarg_0); // Push "this".
generator.Emit(OpCodes.Ldfld, fieldBuilder); // Pop this and push field.
sourceWriter.EmitSourceLine(generator, "LOAD ARGS");
for (int i = 0; i < invocationData.ParameterCount; ++i)
ILHelpers.EmitLdarg(generator, i + 1);
sourceWriter.EmitSourceLine(generator, "INVOKE DELEGATE");
generator.EmitCall(OpCodes.Callvirt, invocationData.InvokeMethod, null); // Call the "Invoke" method.
sourceWriter.EmitSourceLine(generator, "RETURN");
generator.Emit(OpCodes.Ret); // Return.
Debug.Assert(0 == string.Compare(fieldName, fieldBuilder.Name, StringComparison.Ordinal));
return fieldBuilder;
}
private static MethodBuilder GenerateInvokeMethod(SourceWriter sourceWriter, TypeBuilder typeBuilder, InvocationData invocationData)
{
Debug.Assert(null != sourceWriter);
Debug.Assert(null != typeBuilder);
Debug.Assert(null != invocationData);
// Create method.
MethodBuilder methodBuilder = typeBuilder.DefineMethod(
Constants.InvokeMethodName,
MethodAttributes.Private,
invocationData.InvokeMethod.ReturnType,
invocationData.GetParameterTypes());
ILGenerator generator = methodBuilder.GetILGenerator();
sourceWriter.EmitFunctionHeader(Constants.InvokeMethodName);
// Declare local object array.
sourceWriter.EmitSourceLine(generator, "DECLARE LOCAL");
generator.DeclareLocal(typeof(object[]));
// Create an array of three objects and assign it to the local variable.
sourceWriter.EmitSourceLine(generator, "CREATE LOCAL");
ILHelpers.EmitLdc_I4(generator, invocationData.ParameterCount);
generator.Emit(OpCodes.Newarr, typeof(object)); // Create array of objects of size given by value on top of stack (i.e. 3) and push result onto stack.
generator.Emit(OpCodes.Stloc_0); // Store the result (i.e. the newly created array) in the local variable.
// Marshal in- and ref-parameters.
for (int i = 0; i < invocationData.ParameterCount; ++i)
{
ParameterData parameter = invocationData.GetParameter(i);
switch (parameter.Kind)
{
case ParameterKind.In:
sourceWriter.EmitSourceLine(generator, "MARSHAL ARG {0} [{1}]", i, parameter.Kind);
generator.Emit(OpCodes.Ldloc_0); // Push local array variable onto stack.
ILHelpers.EmitLdc_I4(generator, i); // Push array index onto stack.
ILHelpers.EmitLdarg(generator, i + 1); // Push corresponding argument's value onto stack.
generator.Emit(OpCodes.Stelem_Ref);
break;
case ParameterKind.Out:
break;
case ParameterKind.Ref:
sourceWriter.EmitSourceLine(generator, "MARSHAL ARG {0} [{1}]", i, parameter.Kind);
generator.Emit(OpCodes.Ldloc_0); // Push local array variable onto stack.
ILHelpers.EmitLdc_I4(generator, i); // Push array index onto stack.
ILHelpers.EmitLdarg(generator, i + 1); // Push corresponding argument's value or address onto stack.
generator.Emit(OpCodes.Ldind_Ref); // Fetch the value of by-reference values.
generator.Emit(OpCodes.Stelem_Ref);
break;
default:
Debug.Fail("Unexpected");
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
}
}
// Invoke this.GetType(). This leaves the Type instance on the stack.
sourceWriter.EmitSourceLine(generator, "CALL GetType");
generator.Emit(OpCodes.Ldarg_0); // Push "this" onto stack.
MethodInfo getTypeMethod = typeof(object).GetMethod("GetType");
if (null == getTypeMethod)
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
generator.EmitCall(OpCodes.Call, getTypeMethod, null);
// Invoke this.GetType().InvokeMember(Constants.PrivateInvokeMethodName, BindingFlags, null, target, args). This leaves the return value on the stack.
sourceWriter.EmitSourceLine(generator, "CALL InvokeMember");
generator.Emit(OpCodes.Ldstr, Constants.SpecificInvokeMethodName);
generator.Emit(OpCodes.Ldc_I4, (int)(BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod));
generator.Emit(OpCodes.Ldnull);
generator.Emit(OpCodes.Ldarg_0); // Push "this" onto stack.
generator.Emit(OpCodes.Ldloc_0); // Push local array variable onto stack.
MethodInfo invokeMemberMethod = typeof(Type).GetMethod("InvokeMember", BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(string), typeof(BindingFlags), typeof(Binder), typeof(object), typeof(object[]) }, null);
if (null == invokeMemberMethod)
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
generator.EmitCall(OpCodes.Callvirt, invokeMemberMethod, null);
// Unmarshal ref- and out-parameters.
for (int i = 0; i < invocationData.ParameterCount; ++i)
{
ParameterData parameter = invocationData.GetParameter(i);
switch (parameter.Kind)
{
case ParameterKind.In:
break;
case ParameterKind.Out:
case ParameterKind.Ref:
sourceWriter.EmitSourceLine(generator, "UNMARSHAL ARG {0} [{1}]", i, parameter.Kind);
ILHelpers.EmitLdarg(generator, i + 1); // Push argument 2's address onto stack.
generator.Emit(OpCodes.Ldloc_0); // Push local array variable onto stack.
ILHelpers.EmitLdc_I4(generator, i); // Push array index 1 onto stack.
generator.Emit(OpCodes.Ldelem_Ref); // Load element from array and pop arguments off stack.
generator.Emit(OpCodes.Stind_Ref); // Store elemtn to argument 2.
break;
default:
Debug.Fail("Unexpected");
throw new InvalidOperationException(Properties.Resources.InvalidOperationExceptionBadCodeGeneration);
}
}
// Pop return value off stack.
if (typeof(void) == invocationData.InvokeMethod.ReturnType)
generator.Emit(OpCodes.Pop);
// Return.
sourceWriter.EmitSourceLine(generator, "RETURN");
generator.Emit(OpCodes.Ret);
return methodBuilder;
}
}
Comments
- Anonymous
November 11, 2008
The comment has been removed