Fun with IronPython and Cecil

Don't miss Part II of this tutorial

What is Cecil:

"Cecil is a library written by Jb Evain (http://evain.net/blog/) to generate and inspect programs and libraries in the ECMA CIL format. It has full support for generics, and support some debugging symbol format.
In simple English, with Cecil, you can load existing managed assemblies, browse all the contained types, modify them on the fly and save back to the disk the modified assembly. "

Pasted from <http://www.mono-project.com/Cecil>

In this tutorial, I spent a couple of hours playing around Cecil's functionality using IronPython. Basically, I am trying to use Cecil to find different methods that are called from a given method. Sounds interesting? Then pelase read ahead.

Download and Install IronPython and Cecil before going any further. Although you can get away with just downloading IronPython as I have included the Cecil dll inside the solution.

The first thing we'll do is to create a simple Class Library project in C# which we'll inspect using Cecil. The source code for CecilCase.dll is pretty simple and listed below.

You can download the whole solution.

We start with a couple of classes named:  MainCase.cs and SecondCase.cs. Ignore the method names :)

// MainCase.cs
public class MainCase
    {
        public void PublicMethod()
        {
            Console.WriteLine("Hello");
            PrivateMethod();

            new SecondCase().Help("Help me");
        }

        public void AddMethod()
        {
        }

        private void PrivateMethod()
        {
        }

        public void MethodWithArgument(string name)
        {
            Console.WriteLine(name);
        }
    }

// SecondCase.cs
class SecondCase
    {
        public void Help(string message)
        {
            Console.WriteLine(message);
        }
    }

Compile and Build the dll. Now Open IronPython shell.



Make sure that you are in the same directory where you have CecilCase.dll output. Also remember to copy Mono.Cecil.dll which you have downloaded earlier in the same directory.

We start with adding CLR support and adding Reference to the Mono.Cecil dll.

>> import clr
>> clr.AddReference("Mono.Cecil")

As we are testing therefore import all types .

>> from Mono.Cecil import *

Use the dir() command to see the currently loaded types.

>> dir()

The next thing we need to do is to load the assembly. From the FAQ, you can see that we need to use the AssemblyFactory class to load the target assembly.

>> myAsm = AssemblyFactory.GetAssembly("CecilCase.dll")
>> myAsm
<Mono.Cecil.AssemblyDefinition object at 0x000000000000002B [CecilCase, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]>

Types:

Now we get all the types in that assembly. Count ??
>>> myAsm.MainModule.Types.Count
3

3 types???. I thought I created only two. Let's see what are these

>>> for caseType in myAsm.MainModule.Types:
...     print caseType
...
<Module>
CecilCase.MainCase
CecilCase.SecondCase

So <Module> was the hidden type. We'll ignore it from now on.

>>> for caseType in myAsm.MainModule.Types:
...     if caseType.Name != '<Module>':
...         print caseType.Name
...
MainCase
SecondCase

Methods:

Getting the methods of a particular type is also easy. Starting with MainCase class.

>>> mainCaseType = myAsm.MainModule.Types[1]
>>> mainCaseType
<Mono.Cecil.TypeDefinition object at 0x000000000000002D [CecilCase.MainCase]>

Remember the [0] type is the <Module>. So now we have the MainCase type definition.

>>> mainCaseType.Methods.Count
4
>>> for met in mainCaseType.Methods:
...     print met.Name
...
PublicMethod
AddMethod
PrivateMethod
MethodWithArgument

To get the full method definition:

>>> for met in mainCaseType.Methods:
...     print met
...
System.Void CecilCase.MainCase::PublicMethod()
System.Void CecilCase.MainCase::AddMethod()
System.Void CecilCase.MainCase::PrivateMethod()
System.Void CecilCase.MainCase::MethodWithArgument(System.String)

Method Body:

To see inside the method, we'll start with the first method which is MainCase.PublicMethod()

>>> pMet = mainCaseType.Methods[0]
>>> pMet
<Mono.Cecil.MethodDefinition object at 0x000000000000002F [System.Void CecilCase
.MainCase::PublicMethod()]>

To see the different methods, properties that pMet supports, try dir ()

>>> dir(pMet)
['Accept', 'Attributes', 'Body', 'CallingConvention', 'Cctor', 'Clone', 'Ctor',
'CustomAttributes', 'DeclaringType', 'Equals', 'ExplicitThis', 'Finalize', 'Gene
ricParameters', 'GetHashCode', 'GetSentinel', 'GetType', 'HasBody', 'HasThis', '
ImplAttributes', 'IsAbstract', 'IsConstructor', 'IsFinal', 'IsHideBySignature',
'IsInternalCall', 'IsNewSlot', 'IsRuntime', 'IsRuntimeSpecialName', 'IsSpecialNa
me', 'IsStatic', 'IsVirtual', 'MakeDynamicType', 'MemberwiseClone', 'MetadataTok
en', 'Name', 'Overrides', 'PInvokeInfo', 'Parameters', 'RVA', 'Reduce', 'Referen
ceEquals', 'ReturnType', 'SecurityDeclarations', 'SemanticsAttributes', 'This',
'ToString', '__class__', '__doc__', '__init__', '__module__', '__new__', '__redu
ce__', '__reduce_ex__', '__repr__', '__str__']

The one we are interested now is the Body.

>>> pMet.Body
<MethodBody object at 0x0000000000000034>
>>> dir(pMet.Body)
['Accept', 'CilWorker', 'CodeSize', 'Equals', 'ExceptionHandlers', 'Finalize', '
GetHashCode', 'GetType', 'InitLocals', 'Instructions', 'MakeDynamicType', 'MaxSt
ack', 'MemberwiseClone', 'Method', 'Reduce', 'ReferenceEquals', 'Scopes', 'Simpl
ify', 'ToString', 'Variables', '__class__', '__doc__', '__init__', '__module__',
 '__new__', '__reduce__', '__reduce_ex__', '__repr__']

Now we are getting into the instructions, which is a collection object. So looping through it we will get:

>>> for ins in pMet.Body.Instructions:
...     print ins
...
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction
Mono.Cecil.Cil.Instruction

Ok, it prints out the object name as expected. If you're trying it out yourself then you'll find that the intellisense is not working for ins as it is from Mono.Cecil.Cil namespace. So lets add all types from this namespace now.

>>> from Mono.Cecil.Cil import *

>>> for ins in pMet.Body.Instructions:
...     print ins.OpCode.Name
...
nop
ldstr
call
nop
ldarg.0
call
nop
newobj
ldstr
call
nop
ret

Comparing it with the output from Reflector:

Cool. We are getting there. In the above scenario we have 3 call statements which we are going to focus on from here.

>>> for ins in pMet.Body.Instructions:
...     if ins.OpCode.Name == 'call':
...         ins.Operand
...
<Mono.Cecil.MethodReference object at 0x000000000000003F [System.Void System.Console::WriteLine(System.String)]>
<Mono.Cecil.MethodDefinition object at 0x0000000000000031 [System.Void CecilCase.MainCase::PrivateMethod()]>
<Mono.Cecil.MethodDefinition object at 0x000000000000003D [System.Void CecilCase.SecondCase::Help(System.String)]>

So the Operand method gives all the details. Now, we need to get the method definition objects so that we can recursively go through the child methods. For that, we define a method which we pass a MethodDefinition object and that prints the child call elements in it.

>>> def parseMethodBody(methodDef):
...     for ins in methodDef.Body.Instructions:
...         if ins.OpCode.Name == 'call':
...             print ins.Operand
...

From above, we know the ins.Operand returns MethodDefinition/MethodReference. We don't know the different between MethodDefinition/MethodReference yet.

>>> for ins in pMet.Body.Instructions:
...     if ins.OpCode.Name == 'call':
...         parseMethodBody(ins.Operand)
...
Traceback (most recent call last):
  File , line 0, in <stdin>##168
  File , line 0, in parseMethodBody
AttributeError: 'MethodReference' object has no attribute 'Body'

So, we now know that MethodReference doesn't have a Body there we'll ignore it for now by changing the parseMethodBody method.

>>> def parseMethodBody(mDef):
...     if isinstance(mDef, MethodDefinition):
...         for ins in mDef.Body.Instructions:
...             if ins.OpCode.Name == 'call':
...                 print ins.Operand
...

>>> for ins in pMet.Body.Instructions:
...     if ins.OpCode.Name == 'call':
...         parseMethodBody(ins.Operand)
...

By running the code, we didn't get any results because stupidly I didn't add the required code in the SecondCase.Help() method.  Lets add it now.

// SecondCase.cs after adding another method
class SecondCase
    {
        public void Help(string message)
        {
            Console.WriteLine(message);
            HelpMeToo(message);
        }

        public void HelpMeToo(string message2)
        {
            Console.WriteLine(message2);
        }
    }

You'll again need to add the assembly by using the AssemblyFactory and come to the point where we were before adding the HelpMeToo method.  Let's Recap here to test your memory.

  1. Load the assembly using AssemblyFactory after adding reference to Mono.Cecil
  2. Get the required Type
  3. Get the required Method
  4. Get the Instructions (only call)
  5. Get the Operand of each call statement.  

Continuing from where we were:

>>> for inst in pMet.Body.Instructions:
...     if inst.OpCode.Name == 'call':
...         parseMethodBody(inst.Operand)
...
System.Void System.Console::WriteLine(System.String)
System.Void CecilCase.SecondCase::HelpMeToo(System.String)

You can see that we have the list of methods which are called from SecondCase::Help() method 

Similarly, we can develop a recursive function to iterate through the methods. But I think this is enough for now and
maybe I'll try that in the next exercise.

The code above is also available as a ironpython script which can run through the command line to get the same result. The script is in the solution file (Output folder) or view it here.

Ø ipy CecilPlay.py

Hope you like this tutorial. If you have any question or comment then pleae use the form below.

Don't miss Part II of this tutorial

No Comments