Click here to Skip to main content
15,867,756 members
Articles / Programming Languages / C#

Dynamic Runtime Delegates Conversion

Rate me:
Please Sign up or sign in to vote.
4.81/5 (10 votes)
30 May 2017CPOL7 min read 14K   77   5   14
Explanation how to convert from one delegate type to second delegate of unknown or dynamically created type when their signatures match.

Introduction

Delegates can be converted easily to another type if you know types of those delegates. But if you do not know, it is impossible without really complicated code.

Solution

In the example, let us consider the following console application.

C#
class Program
{
    static void Main(string[] args)
    {
    }

    public static event CustomEvent Event;

    protected virtual void OnEvent()
    {
        Event?.Invoke(this, EventArgs.Empty);
    }
}

internal delegate void CustomEvent(object sender, EventArgs args);

Adding handler for CustomeEvent is pretty easy.

C#
Event += (sender, eventArgs) => { };

Right? What if we want to add as handler already existing delegate? I.e. Action<> delegate? We can't just assign one to the another.

C#
Action<object,EventArgs> onEvent = (sender, eventArgs) => { };
Event += onEvent;

Coded like this causes a compile error:

Image 1

Compiler cannot convert one delegate to another, with different type, implicitly for you. You have to help it a little. Delegates types are really just classes that envelopes method pointer. If we want to convert one delegate of known type to another one with known type, you have to use a constructor.

C#
Action<object,EventArgs> onEvent = (sender, eventArgs) => {};
Event += new CustomEvent(onEvent);

The above code compiles and runs perfectly fine.

There is also another possibility without using constructor. Instead, we can assign method with compatible signature to our event.

C#
Action<object,EventArgs> onEvent = (sender, eventArgs) => {};
Event += onEvent.Invoke;

This is the same as assigning some other method to handler like it is done the most usual way.

C#
static void Handler(object sender, EventArgs e){}
Event += Handler;

This is really simple. So where is the problem? Right?

Things get more complicated when there is no clear knowledge of an event type in the time of writing code and during compilation of program. For example, things are also moderately easy if you are sure that event is of EventHandler<> type.

For example, if we create two more events based on generic EventHandler<> type, we also can easily assign handlers to them.

C#
public class Events
{
    public event EventHandler<EventArgs> Event1;
    public event EventHandler<UnhandledExceptionEventArgs> Event2;
}

Action<object, EventArgs> handler = (sender, eventArgs) => { };
events.Event1 += handler.Invoke;
events.Event2 += handler.Invoke;

Assigning handler delegate to both events works because UnhandledExceptionEventArgs is more detailed type then its base type EventArgs, so there is no need for more complex conversion. It will also works if handler has both arguments of type object because object is even more general than EventArgs type.

C#
Action<object, object> handler = (sender, eventArgs) => { };
events.Event1 += handler.Invoke;
events.Event2 += handler.Invoke;

This gets even more problematic when assigning method signature is not 100% compatible. For example, if handler looks like below:

C#
Action<Events, EventArgs> noObjectHandler = (sender, eventArgs) => { };

For that, we need more complex conversion.

C#
private static EventHandler<T> ConvertToEventHandler<S,T>(Action<S, T> d)
{
    return (s, e) => { d((S)s, e); };
}
C#
events.Event1 += ConvertToEventHandler<Events, EventArgs>(noObjectHandler);
events.Event2 += ConvertToEventHandler<Events, UnhandledExceptionEventArgs>(noObjectHandler);

Still, it is only conversion from Action<,> to EventHandler<>. If you want to convert to other, custom type of delegate, it is no longer possible to do it this way.

For example, we cannot create delegate if we do not know its exact type.

C#
public TDelegate CreateDelegate<TDelegate>() where TDelegate : Delegate, new()
{
    return new TDelegate();
}

Creating delegate this way is not permitted in C#. So for example, if we would have event like this:

C#
public class Events
{
    public event CustomEvent Event;
}

We cannot assign event handler if we have access to event type only by reflection.

C#
var t = typeof(CustomEvent);
Activator.CreateInstance(t, handler.Invoke);

This will not work since handler.Invoke is not a valid variable. The only constructor for type of base of Delegate, that is available through reflection, takes method pointer as a parameter.

Image 2

To use this constructor, we need to call it via Reflection like below:

HTML
t.GetConstructors()[0].Invoke(new object[] { handler, handler.Method.MethodHandle.Value });

Or at least we can try to use it, since it will throw a FatalExecutionEngineError in our application.

Image 3

It is because we cannot obtain method handle in managed code as it is explained in MethodInfo.MethodHandle documentation.

This property is for access to managed classes from unmanaged code and should not be called from managed code.

How can we use this constructor then?

We need to use this the same way C# compiler use it - from IL.

Let us inspect what IL code is generated by code similar to below:

HTML
Event += (sender, eventArgs) => { };

IL code will look similar to this:

IL_0001:  ldsfld     class DelegatesConversion.CustomEvent DelegatesConversion.Program/'<>c'::'<>9__7_0'
IL_0006:  dup
IL_0007:  brtrue.s   IL_0020
IL_0009:  pop
IL_000a:  ldsfld     class DelegatesConversion.Program/'<>c' DelegatesConversion.Program/'<>c'::'<>9'
IL_000f:  ldftn      instance void DelegatesConversion.Program/'<>c'::'<Main>b__7_0'(object,
                                     class [mscorlib]System.EventArgs)
IL_0015:  newobj     instance void DelegatesConversion.CustomEvent::.ctor(object,
                                                                            native int)
IL_001a:  dup
IL_001b:  stsfld     class DelegatesConversion.CustomEvent DelegatesConversion.Program/'<>c'::'<>9__7_0'
IL_0020:  call       void DelegatesConversion.Program::add_Event
                     (class DelegatesConversion.CustomEvent)

Let us inspect it line by line.

ldsfld IL code loads field value on top of a stack. dup IL code duplicates this value on the stack. brtrue.s instruction checks if value on top of a stack evaluates to boolean true and transfers control to another address. If it evaluates to false, then next address code is executed - in this case, pop code, which removes first value loaded, value of field (the one that was duplicated), because it is null anyway (evaluates to false). In short, the program checks if compiler generated field with value of (sender, eventArgs) => { }; was already set or it has to be set for the first time. It is done this way because compiler creates instance of a delegate only once and then sets to an event the same instance every time you call += code with the same handler. This is compiler code optimization and we are not really interested in it. The next codes are the ones that do all the magic we need. Codes at address 000a and 000f load parameters for delegate constructor executed by newobj IL code. First parameter is an instance with method we want to assign to delegate created by constructor. Second parameter is a pointer to this method. Method will be executed by delegate created by constructor. newobj code executes constructor, creates this new delegate and puts it onto the stack. Next code duplicates this value for the stsfld code, which stores this value to a field for future use. Last one takes duplicated value of a field to add it to event as a handler by using event add accessor. This way, C# compiler adds new handler to an event.

As you can see for converting one type of delegate to another, we only need to:

  1. execute ldsfld or similar code to load pointer to an instance with method for delegate
  2. execute ldftn for a method pointer
  3. create new delegate by calling constructor via newobj code

This way, we can create delegate that we can use by just doing += with and event.

C#
Event += d;

To create IL code via C#, we need a little more complicated code. For example, we can use DynamicMethod type for that.

C#
var t = typeof(CustomEvent);
var dynamicMethod = new DynamicMethod("converter", typeof(CustomEvent), 
    new[] { typeof(Action<object, EventArgs>) });
var generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldftn, typeof(Action<object, EventArgs>).GetMethod("Invoke"));
generator.Emit(OpCodes.Newobj, t.GetConstructors()[0]);
generator.Emit(OpCodes.Ret);
var converter = (Func<Action<object, EventArgs>, CustomEvent>)
    dynamicMethod.CreateDelegate(typeof(Func<Action<object, EventArgs>, CustomEvent>));

It is pretty much the same code as above, but instead ldsfld, ldarg.0 code is used, which loads first argument of a method instead of a field. That way, our newly created converter uses parameter as source for conversion.

To use it, we only need to call it with parameter of Action<object, EventArgs>.

C#
Action<object, EventArgs> onEvent = (sender, eventArgs) =>
{
    var a = 1;
};
var d = converter(onEvent);
Event += d;
OnEvent();

After running this code, we can see that invoking an event causes to invoke onEvent delegate.

Image 4

Thing is, from an experience, I can tell that using a DynamicMethod does not work all the time. For some uses on some versions of .NET, it throws a critical error. It is safer instead to use AssemblyBuilder.

HTML
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly
(new AssemblyName("dynamicAssembly"), AssemblyBuilderAccess.RunAndCollect);
var module = assemblyBuilder.DefineDynamicModule("module");
var typeBuilder = module.DefineType("converter_type");
var methodBuilder = typeBuilder.DefineMethod("converter", 
MethodAttributes.Static | MethodAttributes.Public |
    MethodAttributes.Final, CallingConventions.Standard, typeof(CustomEvent), new[]
{
    typeof(Action<object, EventArgs>)
});
var generator = methodBuilder.GetILGenerator();
var t = typeof(TDest);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldftn, typeof(Action<object, EventArgs>).GetMethod("Invoke"));
generator.Emit(OpCodes.Newobj, t.GetConstructors()[0]);
generator.Emit(OpCodes.Ret);
var type = typeBuilder.CreateType();
var converter = (Func<Action<object, EventArgs>, CustomEvent>)type.GetMethod
("converter").CreateDelegate(typeof(Func<Action<object, EventArgs>, CustomEvent>));

This is not that much more complicated. Created converter is used in the same way as the one created with DynamicMethod.

Making things better, we can create generic method for conversion:

C#
private static Func<TSource, TDest> CreateConverter<TSource, TDest>()
{
    var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly
    (new AssemblyName("dynamicAssembly"),
        AssemblyBuilderAccess.RunAndCollect);
    var module = assemblyBuilder.DefineDynamicModule("module");
    var typeBuilder = module.DefineType("converter_type");
    var methodBuilder = typeBuilder.DefineMethod("converter", 
    MethodAttributes.Static | MethodAttributes.Public |
    MethodAttributes.Final, CallingConventions.Standard,
        typeof(TDest), new[] { typeof(TSource) });
    var generator = methodBuilder.GetILGenerator();
    var t = typeof(TDest);
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldftn, typeof(TSource).GetMethod("Invoke"));
    generator.Emit(OpCodes.Newobj, t.GetConstructors()[0]);
    generator.Emit(OpCodes.Ret);
    var type = typeBuilder.CreateType();
    var converter = (Func<TSource, TDest>)type.GetMethod
    ("converter").CreateDelegate(typeof(Func<TSource, TDest>));
    return converter;
}

This way, we can create converters in a more flexible way.

C#
var converter = CreateConverter<Action<object,EventArgs>, CustomEvent>();

To use it, we just have to call it with the appropriate parameter.

C#
Action<object, EventArgs> onEvent = (sender, eventArgs) =>
{
    var a = 1;
};
var d = converter(onEvent);
Event += d;
OnEvent();

This code compiles and run perfectly without any problems.

But it is barely usable. After all, what is the point of creating some complicated converter since we can just do this:

C#
Event += onEvent.Invoke;

It makes much more sense when we do not have access to type of an event during compile time - when in example type is loaded dynamically or dynamically created during runtime. Then we cannot use C# standard way for adding handler to event. For this case, converter is a must. But then, it does not makes sense to require destination type if the only use case is when do not have it. For that, we need to change CreateConverter method.

C#
public static Func<TSource, Delegate> 
CreateConverter<TSource>(Type destinationType)
{
    var sourceType = typeof(TSource);
    var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly
    (new AssemblyName("dynamicAssembly"),
        AssemblyBuilderAccess.RunAndCollect);
    var module = assemblyBuilder.DefineDynamicModule("module");
    var typeBuilder = module.DefineType("converter_type");
    var methodBuilder = typeBuilder.DefineMethod
    ("converter", MethodAttributes.Static | MethodAttributes.Public |
    MethodAttributes.Final, CallingConventions.Standard,
        destinationType, new[] { sourceType });
    var generator = methodBuilder.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldftn, sourceType.GetMethod("Invoke"));
    generator.Emit(OpCodes.Newobj, destinationType.GetConstructors()[0]);
    generator.Emit(OpCodes.Ret);
    var type = typeBuilder.CreateType();
    var converter = (Func<TSource, Delegate>)type.GetMethod
    ("converter").CreateDelegate(typeof(Func<TSource, Delegate>));
    return converter;
}

Now we get generic method with one less type parameter, which is unknown anyway. Below is an example how can we add created delegate as a handler to an event, which is inaccessible during compile time, through a Reflection.

C#
Action<object, EventArgs> onEvent = (sender, eventArgs) =>
{
    var a = 1;
};
var d = converter(onEvent);
var add = typeof(Program).GetEvent("Event", BindingFlags.Static | 
BindingFlags.NonPublic).GetAddMethod(true);
add.Invoke(null, new object[] { d });
OnEvent();

That is it. Working solution for dynamic delegates conversion. But we can make things prettier, i.e., extract method to a separate type.

C#
public static class DelegatesConversion
{
    public static Func<TSource> CreateConverter<TSource>(Type destinationType)
    {
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly
        (new AssemblyName("dynamicAssembly"),
            AssemblyBuilderAccess.RunAndCollect);
        var module = assemblyBuilder.DefineDynamicModule("module");
        var typeBuilder = module.DefineType("converter_type");
        var methodBuilder = typeBuilder.DefineMethod
        ("converter", MethodAttributes.Static | MethodAttributes.Public |
        MethodAttributes.Final, CallingConventions.Standard,
            destinationType, new[] { typeof(TSource) });
        var generator = methodBuilder.GetILGenerator();
        generator.Emit(OpCodes.Ldarg_0);
        generator.Emit(OpCodes.Ldftn, typeof(TSource).GetMethod("Invoke"));
        generator.Emit(OpCodes.Newobj, destinationType.GetConstructors()[0]);
        generator.Emit(OpCodes.Ret);
        var type = typeBuilder.CreateType();
        var converter = (Func<TSource, Delegate>)type.GetMethod
        ("converter").CreateDelegate(typeof(Func<TSource, Delegate>));
        return converter;
    }
}

Another thing is that sometimes, we do not need to know or do not know source type too (because we are converting two dynamic types of delegates). For this, we can create another overload of CreateConverter method without type parameters.

C#
public static Func<TSource, 
Delegate> CreateConverter<TSource>(Type destinationType)
{
    var sourceType = typeof(TSource);
    return CreateConverterInternal<Func<TSource, 
    Delegate>>(destinationType, sourceType);
}

public static Func<Delegate, Delegate> 
CreateConverter(Type sourceType, Type destinationType)
{
    return CreateConverterInternal<Func<Delegate, 
    Delegate>>(destinationType, sourceType);
}

private static TConverterDelegate CreateConverterInternal<TConverterDelegate>
            (Type destinationType, Type sourceType) where TConverterDelegate : class
{
    var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly
    (new AssemblyName("dynamicAssembly"),
        AssemblyBuilderAccess.RunAndCollect);
    var module = assemblyBuilder.DefineDynamicModule("module");
    var typeBuilder = module.DefineType("converter_type");
    var methodBuilder = typeBuilder.DefineMethod("converter", 
    MethodAttributes.Static | MethodAttributes.Public |
    MethodAttributes.Final, CallingConventions.Standard,
        destinationType, new[] { sourceType });
    var generator = methodBuilder.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldftn, sourceType.GetMethod("Invoke"));
    generator.Emit(OpCodes.Newobj, destinationType.GetConstructors()[0]);
    generator.Emit(OpCodes.Ret);
    var type = typeBuilder.CreateType();
    var converter = type.GetMethod("converter").CreateDelegate
    (typeof(TConverterDelegate)) as TConverterDelegate;
    return converter;
}

We extracted code from both overloads to one single, internal method, since they would consist from very similar code anyway. This way is much cleaner.

One last touch to do, would be to validate delegates methods signatures.

C#
private static TConverterDelegate CreateConverterInternal<TConverterDelegate>
            (Type destinationType, Type sourceType) where TConverterDelegate : class
{
    var sourceParams = GetInvokeMethodParams(sourceType);
    var destParams = GetInvokeMethodParams(destinationType);
    if (ValidateSignatures(sourceParams, destParams))
    {
        return null;
    }
    var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly
    (new AssemblyName("dynamicAssembly"),
        AssemblyBuilderAccess.RunAndCollect);
    var module = assemblyBuilder.DefineDynamicModule("module");
    var typeBuilder = module.DefineType("converter_type");
    var methodBuilder = typeBuilder.DefineMethod("converter", 
    MethodAttributes.Static | MethodAttributes.Public |
    MethodAttributes.Final, CallingConventions.Standard,
        destinationType, new[] { sourceType });
    var generator = methodBuilder.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldftn, sourceType.GetMethod("Invoke"));
    generator.Emit(OpCodes.Newobj, destinationType.GetConstructors()[0]);
    generator.Emit(OpCodes.Ret);
    var type = typeBuilder.CreateType();
    var converter = type.GetMethod("converter").CreateDelegate
    (typeof(TConverterDelegate)) as TConverterDelegate;
    return converter;
}

private static bool ValidateSignatures(Type[] sourceParams, Type[] destParams)
{
    if (sourceParams.Length == destParams.Length)
    {
        for (var i = 0; i < sourceParams.Length; i++)
        {
            if (sourceParams[i] != destParams[i])
            {
                return false;
            }
        }
    }
    return false;
}

private static Type[] GetInvokeMethodParams(Type delegateType)
{
    return delegateType.GetMethod("Invoke").
    GetParameters().Select(p => p.ParameterType).ToArray();
}

Delegates are compatible only when their signatures are matching. Since events usually return void, we only need to check if parameters match. This way, converter will not be created for two incompatible delegates.

This is the final version of code. :) You can download the code here or look it up on GitHub.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Poland Poland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionI think you jumped from too simple to too complex. Pin
Paulo Zemek31-May-17 10:59
mvaPaulo Zemek31-May-17 10:59 
AnswerRe: I think you jumped from too simple to too complex. Pin
n.podbielski1-Jun-17 0:52
n.podbielski1-Jun-17 0:52 
GeneralRe: I think you jumped from too simple to too complex. Pin
Paulo Zemek1-Jun-17 3:22
mvaPaulo Zemek1-Jun-17 3:22 
GeneralRe: I think you jumped from too simple to too complex. Pin
n.podbielski8-Jun-17 9:28
n.podbielski8-Jun-17 9:28 
GeneralRe: I think you jumped from too simple to too complex. Pin
Paulo Zemek8-Jun-17 9:59
mvaPaulo Zemek8-Jun-17 9:59 
GeneralRe: I think you jumped from too simple to too complex. Pin
n.podbielski8-Jun-17 10:42
n.podbielski8-Jun-17 10:42 
GeneralRe: I think you jumped from too simple to too complex. Pin
Paulo Zemek8-Jun-17 10:46
mvaPaulo Zemek8-Jun-17 10:46 
GeneralRe: I think you jumped from too simple to too complex. Pin
Paulo Zemek8-Jun-17 10:43
mvaPaulo Zemek8-Jun-17 10:43 
GeneralRe: I think you jumped from too simple to too complex. Pin
n.podbielski8-Jun-17 20:00
n.podbielski8-Jun-17 20:00 
GeneralRe: I think you jumped from too simple to too complex. Pin
Paulo Zemek9-Jun-17 1:42
mvaPaulo Zemek9-Jun-17 1:42 
GeneralRe: I think you jumped from too simple to too complex. Pin
n.podbielski9-Jun-17 23:42
n.podbielski9-Jun-17 23:42 
GeneralRe: I think you jumped from too simple to too complex. Pin
Paulo Zemek10-Jun-17 3:27
mvaPaulo Zemek10-Jun-17 3:27 
GeneralRe: I think you jumped from too simple to too complex. Pin
n.podbielski11-Jun-17 23:59
n.podbielski11-Jun-17 23:59 
GeneralRe: I think you jumped from too simple to too complex. Pin
Paulo Zemek12-Jun-17 7:16
mvaPaulo Zemek12-Jun-17 7:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.