DebuggerTypeProxy for IronPython Old-style Class and Instance
First I want to make it clear that this post means nothing related to IronPython's plan about debugging python code in the future; it serves more as an interesting example to demonstrate DebuggerTypeProxyAttribute and how types can be created dynamically and be used in the VS debugger windows to display information the end users are interested in.
The python code below shows one old style class and some operations on the instance. After downloading the binary zip of IronPython 2.0 Alpha 3, we can create a solution, set it to launch ipy.exe for debugging, and step through the code inside Visual Studio; the locals window may show something like this after hitting the breakpoint.
The local variable alpha3's .NET type is IronPython.Runtime.Types.OldInstance, the way this object is displayed in the locals window is based on that type's fields and properties, which exposes many implementation details. While debugging, what the python users really want to know is the object attributes. My goal is to make python object attributes look like C# object's public fields; how can I achieve this?
DebuggerTypeProxyAttribute allows us to specify a proxy type for the target type. VS debugger uses the proxy type to display the target object. We already see such proxy types a lot: when viewing objects of most collection types, we are actually viewing their proxy type layouts.
In current IronPython implementation, every old style instance is of type I.R.T.OldInstance, so my first step is to define a proxy type "OldInstanceProxy" to expose the object attributes only. Clearly old style instances can have different attribute set of different names; but I can only specify one proxy type for the I.R.T.OldInstance type. [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] is part of the solution to achieve the look-and-feel. The proxy type (OldInstanceProxy) has an object field (m_oldInstance) decorated with that attribute so that m_oldInstance will be hiden in the debugger window, but the fields m_oldInstance has will be shown. What should m_oldInstance look like? A .NET type (let me call it the "real" proxy type) with as many public fields as python object attributes, each having the same name as the attribute name, and we are going to create such types on the fly. Also the "real" proxy type's constructor is emitted with the code to assign these public fields properly. We then instantiate the new type with the original OldInstance object, and assign it to m_oldInstance.
[assembly:DebuggerTypeProxy(
typeof(IronPython.DebuggingSupport.OldInstanceProxy),
TargetTypeName = "IronPython.Runtime.Types.OldInstance, IronPython, Version=2.0.0.300, ...")
]
public class OldInstanceProxy{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public object m_oldInstance;
public OldInstanceProxy(object target) {
try {
OldInstance obj = target as OldInstance;
TypeBuilder tb = CodeGen.CreateTemporaryType();
ConstructorBuilder cb = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(OldInstance) });
ILGenerator il = cb.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, typeof(Object).GetConstructor(Type.EmptyTypes));
foreach (KeyValuePair<object,object> pair in obj.Dictionary) {
string key = pair.Key.ToString();
Type fieldType = pair.Value == null ? typeof(object) : pair.Value.GetType();
FieldBuilder fb = tb.DefineField(key, fieldType, FieldAttributes.Pub.Public);
// emit code for the ctor ...
}
il.Emit(OpCodes.Ret);
m_oldInstance = Activator.CreateInstance(tb.CreateType(), obj);
Few discussions:
- The attached solution also includes another proxy type: OldClassProxy for IronPython.Runtime.Types.OldClass. With that, viewing class attributes feels like viewing C# class static fields. Such coding pattern can be applied to other similar scenarios.
- It is a bit hard to apply this for IronPython new style instances. For each new style class, IronPython creates a new type on the fly. For example, "class C(object): pass" generates a IronPython.Runtime.NewTypes.Object_1. The proxy type (which will create the "real" proxy type) need be created at the same time.
- Types created by Reflection.Emit are not GC-collectible. In my proxy type implementation, a simple caching mechanism is implemented to reuse those "real" proxy types; but considering the python dynamism, object attributes can be added, removed, their values' type can be changed; such "real" proxy type may have to be created frequently, each click to expand in the debugger window could cause one type creation, so use wisely if you use such proxy type in real world scenarios.
- Unlike DebuggerVisualizer where VS provides a way to debug it, it is a bit inconvenient to debug DebuggerTypeProxy code. So I found having the constructor code wrapped inside try-catch and saving the exception if thrown is useful. The exception object can be viewed in the debugger window if the type proxy dll is compiled with the "Debug" configuration. The messages from Debug.WriteLine are helpful too.
- The proxy type implementation depends on the rapidly changing code of the DLR/IronPython project. It is not surprising that the attached solution could be broken in the future. .
The following pictures show the proxy type in action. Compared to the previous picture, now you can easily check out the values of the attribute "url" and "version", as well as the class attribute "project_name" (shown in the first picture). The new attribute "release_date" appears in the second picture after stepping through the breakpoint line.
To use the attached solution, you need download IronPython 2.0 alpha 3 binary zip, and I assume you unzip it to C:\. Your comments are welcome.