Fun with IronPython and Cecil (Part II)

UPDATE: [ Part III with Netron Project ] 

By the end of first exercise, we can list through list of method calls in a particular method body. Now, my target will be to take a assemblyname::typename::method as input then recursively iterate through the method lists and generate a nice looking visualization.

So, we redifine out parseMethodbody method:

>>> def parseMethodBody(mDef, level):
...     if isinstance(mDef, MethodDefinition):
...         print '>>> %d - Parent Method: %s' % (level, mDef)
...         for ins in mDef.Body.Instructions:
...             if ins.OpCode.Name == 'call':
...                 print ins.Operand
...                 nextLevel = level + 1
...                 parseMethodBody(ins.Operand, nextLevel)

Then passing out method definition object pMet with Level 0 yields the following result:

>>> parseMethodBody(pMet, 0)
>>> 0 - Parent Method: System.Void CecilCase.MainCase::PublicMethod()
System.Void System.Console::WriteLine(System.String)
System.Void CecilCase.MainCase::PrivateMethod()
>>> 1 - Parent Method: System.Void CecilCase.MainCase::PrivateMethod()
System.Void CecilCase.SecondCase::Help(System.String)
>>> 1 - Parent Method: System.Void CecilCase.SecondCase::Help(System.String)
System.Void System.Console::WriteLine(System.String)
System.Void CecilCase.SecondCase::HelpMeToo(System.String)
>>> 2 - Parent Method: System.Void CecilCase.SecondCase::HelpMeToo(System.String)
System.Void System.Console::WriteLine(System.String)

Now, lets find out how can we put that in a text file. As it turned out there can be two ways to write files using IronPython. One is the standard .NET way and other is to use the file object in Python. For now, I'll stick to Python.

The following code opens a file for writing, writes some text and close it.

>>> f = open("foo.txt", "w")
>>> f.Write("Hello from IronPython")
>>> f.Close()

So, to write our output to a file. We change the parseMethodBody use the file object.

>>> def parseMethodBody(mDef, level, f):
...     if isinstance(mDef, MethodDefinition):
...         msg = '\r\n>>> %d - Parent Method: %s' % (level, mDef)
...         print msg
...         f.Write(msg)
...         for ins in mDef.Body.Instructions:
...             if ins.OpCode.Name == 'call':
...                     msg = '\r\n' + ins.Operand.ToString()
...                     print msg
...                     f.Write(msg)
...                     nextLevel = level + 1
...                     parseMethodBody(ins.Operand, nextLevel, f)
...

Then before calling the parseMethodBody; we open the file, pass the file object to the method and close it on return.

>>> f = open("methods.txt", "w")
>>> parseMethodBody(pMet, 0, f)
>>> f.close()

Now, we need to turn this text into a diagram. We played with GLEE before so ready to start plugging it here.

print '>>> Drawing the method tree'
graph = Microsoft.Glee.Drawing.Graph("Method Tree")

Now, we create a method to draw the diagram.

>>> def drawMethodTree(mDef, level, g):
...     if isinstance(mDef, MethodDefinition):
...         msg = '\r\n >>> %d - Parent Method: %s' % (level, mDef)
...         for ins in mDef.Body.Instructions:
...             if ins.OpCode.Name == 'call' and ins.OpCode.Name == 'callvirt':
...                 msg = '\r\n' + ins.Operand.ToString()
...                 nextLevel = level + 1
...                 g.AddEdge(mDef.Name, ins.Operand.Name)
...                 g.FindNode(mDef.Name).NodeAttribute.Fillcolor = getColorByLevel(level)
...                 g.FindNode(ins.Operand.Name).NodeAttribute.Fillcolor = getColorByLevel(level)
...                 drawMethodTree(ins.Operand, nextLevel, g)
...

You can see that we are using another custom method called getColorByLevel which is defined below.

>>> def getColorByLevel(level):
            print '>>> at Level: %d' % level
            if level == 0:
                   return Microsoft.Glee.Drawing.Color.Red;
            elif level == 1:
                   return Microsoft.Glee.Drawing.Color.Blue;
            elif level == 2:
                   return Microsoft.Glee.Drawing.Color.Green;
            elif level == 3:
                   return Microsoft.Glee.Drawing.Color.Gray;
            elif level == 4:
                   return Microsoft.Glee.Drawing.Color.LightGreen

The levels are defined without any thinking so you may need to add another level if you get the following exception.

Traceback (most recent call last):
  File C:\Code\Sandbox\CecilBox\Output\CecilPlay.py, line 87, in Initialize
  File C:\Code\Sandbox\CecilBox\Output\CecilPlay.py, line 39, in drawMethodTree
  File C:\Code\Sandbox\CecilBox\Output\CecilPlay.py, line 39, in drawMethodTree
  File C:\Code\Sandbox\CecilBox\Output\CecilPlay.py, line 39, in drawMethodTree
  File C:\Code\Sandbox\CecilBox\Output\CecilPlay.py, line 38, in drawMethodTree
  File , line 0, in set_Fillcolor##52
TypeError: Object None is not of type Microsoft.Glee.Drawing.Color

Once, we have the methods, we then passes the graph object to drawMethodTree()

>>> drawMethodTree(pMet, 0, graph)

The graph object is now populated with edges. Rendering it now simple.

>>> renderer = Microsoft.Glee.GraphViewerGdi.GraphRenderer(graph)
>>> renderer.CalculateLayout()
>>>  bmp = Bitmap(graph.Width, graph.Height, PixelFormat.Format32bppArgb)
>>>  renderer.Render(bmp)
>>>  bmp.Save("IronGlee.png")

We can easily add the call sequence for each method. Here is the changed drawMethodTree() method.

>>> def drawMethodTree(mDef, level, g, callSeq):
                 if isinstance(mDef, MethodDefinition):
                         msg = '\r\n>>> %d - Parent Method: %s' % (level, mDef)
                         for ins in mDef.Body.Instructions:
                                    if ins.OpCode.Name == 'call' or ins.OpCode.Name == 'callvirt':
                                              msg = '\r\n' + ins.Operand.ToString()                                        
                                              nextLevel = level + 1                                
                                              g.AddEdge(mDef.Name, callSeq.ToString(), ins.Operand.Name)
                                              callSeq = callSeq + 1
                                              g.FindNode(mDef.Name).NodeAttribute.Fillcolor = getColorByLevel(level)
                                              g.FindNode(ins.Operand.Name).NodeAttribute.Fillcolor = getColorByLevel(nextLevel)
                                              drawMethodTree(ins.Operand, nextLevel, g, callSeq)

And calling it with initial callSeq = 1

>>> drawMethodTree(pMet, 0, graph, 1)

 

By filtering out System.Console namespace, we can further simplify the graph

You can see how easy it is to integrate and play with IronPython to get the best out of other projects. I still think the diagrams are good but not ideal as it will get cumbersome with more crowded method bodies. Next, we'll see if we can use any other engine to render the output.

Download Complete Solution 

Download Python Script

UPDATE: [ Part III with Netron Project ] 

1 Comment

Comments have been disabled for this content.