다음을 통해 공유


Turn MethodInfo to DynamicMethod

I do not know why anyone ever need this :) but few readers did ask me similar questions before. Solving this problem also demonstrates one more ILReader usage.

To build a DynamicMethod, we can choose either DynamicILGenerator or DynamicILInfo. My first impression of using DynamicILGenerator to emit code is slow: you need make calls to emit ILs instruction by instruction. Actually this approach is more difficult, a topic for future post. I already wrote a post about DynamicILInfo. I do not know the motivation behind the DynamicILInfo class (at that time I was not in the CLR team), but I heard this was a feature request from SQL. 

To turn a MethodInfo to DynamicMethod, we can not simply call SetCode with the byte array returned by the static method body's GetILAsByteArray. Those tokens (if any) are metadata tokens, meaningful only in the scope of the module where that static method lives. The DynamicILInfo.GetTokenFor overloads are designed to build the relationship between an integer number (the new "token") and the runtime member (type, method, field, string, or signature...) in the scope of the dynamic method. We can first make a copy of the static method's IL byte array, and overwrite the old metadata token with the new token from GetTokenFor for each applicable IL instruction. So here comes ILInfoGetTokenVisitor. As you see, we override 6 visit methods in ILInfoGetTokenVisitor, doing the token replacements; the top-level code to prepare new code array looks simple.

class ILInfoGetTokenVisitor : ILInstructionVisitor {
  private DynamicILInfo ilInfo;
  private byte[] code;

  public ILInfoGetTokenVisitor(DynamicILInfo ilinfo, byte[] code) {
    this.ilInfo = ilinfo;
    this.code = code;
  }

  public override void VisitInlineMethodInstruction(InlineMethodInstruction inlineMethodInstruction) {
    OverwriteInt32(ilInfo.GetTokenFor(inlineMethodInstruction.Method.MethodHandle,
       inlineMethodInstruction.Method.DeclaringType.TypeHandle),
       inlineMethodInstruction.Offset + inlineMethodInstruction.OpCode.Size);
  }
  public override void VisitInlineFieldInstruction(InlineFieldInstruction inlineFieldInstruction) { ... }
  public override void VisitInlineSigInstruction(InlineSigInstruction inlineSigInstruction) { ... }
  public override void VisitInlineStringInstruction(InlineStringInstruction inlineStringInstruction) { ... }
  public override void VisitInlineTokInstruction(InlineTokInstruction inlineTokInstruction) { ... }
  public override void VisitInlineTypeInstruction(InlineTypeInstruction inlineTypeInstruction) { ... }
  ...
}

private static void SetCode(MethodInfo method, MethodBody body, DynamicILInfo ilInfo) {
  byte[] code = body.GetILAsByteArray();
  ILReader reader = new ILReader(method);
  ILInfoGetTokenVisitor visitor = new ILInfoGetTokenVisitor(ilInfo, code);
  reader.Accept(visitor);

  ilInfo.SetCode(code, body.MaxStackSize);
}

JAY was asking how to set exceptions. Reflection does not provide API to return the exception blob directly, instead the exception information is exposed through the MethodBody.ExceptionHandlingClauses property. With this ExceptionHandlingClause list, we can follow the format described by Ecma 335 (Part 2, 25.4.5 and 25.4.6) and build the exception blob from scratch. To avoid worrying about the restriction of the small version of exception handling clauses, the exception array generated below is with the fat form layout. See some comments inside the code.

private static void SetExceptions(MethodBody body, DynamicILInfo ilInfo) {
  IList<ExceptionHandlingClause> ehcs = body.ExceptionHandlingClauses;
  int ehCount = ehcs.Count;
  if (ehCount == 0) return;

  // Let us do FAT exception header
  int size = 4 + 24 * ehCount;
  byte[] exceptions = new byte[size];

  exceptions[0] = 0x01 | 0x40; //Offset: 0, Kind: CorILMethod_Sect_EHTable | CorILMethod_Sect_FatFormat
  OverwriteInt32(size, 1, exceptions); // Offset: 1, DataSize: n * 24 + 4

  int pos = 4;
  foreach (ExceptionHandlingClause ehc in ehcs) {
    // Flags, TryOffset, TryLength, HandlerOffset, HandlerLength: all size 4
    OverwriteInt32((int)ehc.Flags, pos, exceptions); pos += 4;
    OverwriteInt32(ehc.TryOffset, pos, exceptions); pos += 4;
    OverwriteInt32(ehc.TryLength, pos, exceptions); pos += 4;
    OverwriteInt32(ehc.HandlerOffset, pos, exceptions); pos += 4;
    OverwriteInt32(ehc.HandlerLength, pos, exceptions); pos += 4;

    // ClassToken or FilterOffset
    switch (ehc.Flags) {
      case ExceptionHandlingClauseOptions.Clause:
        int token = ilInfo.GetTokenFor(ehc.CatchType.TypeHandle);
        OverwriteInt32(token, pos, exceptions);
        break;
      case ExceptionHandlingClauseOptions.Filter:
        OverwriteInt32(ehc.FilterOffset, pos, exceptions);
        break;
      case ExceptionHandlingClauseOptions.Fault:
        throw new NotSupportedException("dynamic method does not support fault clause");
      case ExceptionHandlingClauseOptions.Finally:
        break;
    }
    pos += 4;
  }

  ilInfo.SetExceptions(exceptions);
}

If you are interested in the details, you may download the attachment and read the whole source code closely.

A related question is how to build a dynamic method from other sources. DynamicMethod is not Serializable, .NET framework built-in serializing/deserializing support will not work. One approach I can think of is to preserve the code array, the members/strings/... (in order to call GetTokenFor to get new tokens) used and their matching offsets (with one scan of the ILs). MethodInfo, FieldInfo, Type are marked "Serializable", but not sure whether this is the right way to save/restore those members. If the method handles exception, we can save the pre-calculated exception blob, and remember where we need replace token for what exception type.

Finally, let us play with the turn-static-method-to-dynamic-method call, and show off some IronPython code. The first 3 lines bring in the DynamicMethodHelper class. Then we use Reflection to get DateTime.get_Now's MethodInfo (sGetNow), and convert it to DynamicMethod (dGetNow) with "DynamicMethodHelper.ConvertFrom". The last part of the code is interesting: it converts the method "DynamicMethodHelper.ConvertFrom" to DynamicMethod (dConvert), and use that dynamic method (dConvert) to convert sGetNow to another dynamic method (dGetNow2). Just fyi: DateTime.get_Now has 5 instructions, DynamicMethodHelper.ConvertFrom has ~80 instructions and exception handlings clauses.

>>> import clr, System
>>> clr.AddReference('ClrTest.Reflection.DynamicMethodHelper.dll')
>>> from ClrTest.Reflection import *
>>>
>>> clr.GetClrType(System.DateTime).GetMethod("get_Now")
<System.Reflection.RuntimeMethodInfo object at 0x000000000000002B [System.DateTime get_Now()]>
>>> sGetNow = _
>>> DynamicMethodHelper.ConvertFrom(sGetNow)
<System.Reflection.Emit.DynamicMethod object at 0x000000000000002C [System.DateTime get_Now()]>
>>> dGetNow = _
>>> dGetNow.Invoke(None, None)
<System.DateTime object at 0x000000000000002D [11/7/2006 11:40:49 AM]>
>>>
>>> clr.GetClrType(DynamicMethodHelper).GetMethod("ConvertFrom")
<System.Reflection.RuntimeMethodInfo object at 0x000000000000002E [System.Reflection.Emit.DynamicMethod ConvertFrom(System.Reflection.MethodInfo)]>
>>> sConvert = _
>>> DynamicMethodHelper.ConvertFrom(sConvert)
<System.Reflection.Emit.DynamicMethod object at 0x000000000000002F [System.Reflection.Emit.DynamicMethod ConvertFrom(System.Reflection.MethodInfo)]>
>>> dConvert = _
>>>
>>> dConvert.Invoke(None, System.Array[object]([sGetNow]))
<System.Reflection.Emit.DynamicMethod object at 0x0000000000000030 [System.DateTime get_Now()]>
>>> dGetNow2 = _
>>> dGetNow2.Invoke(None, None)
<System.DateTime object at 0x0000000000000031 [11/7/2006 11:40:59 AM]>
>>>

DynamicMethodHelper.cs

Comments