다음을 통해 공유


Turning your .NET object models dynamic for IronPython

Say you want to interop with a .NET library but you also want it to behave like objects in dynamic languages do. You want to be able to add/delete methods/properties to the object dynamically. In python you can do something like this:

 class x(object):
    pass

y = x()
y.z = 42
dir(y)

And the dir(y) will contain z in it. Now if x were a .NET class instead of a python class would you still be able to do the same? Let’s try a simple .NET class

 public class TestExt
{
}

From IronPython you would do something like this:

 import clr
clr.AddReference("TestExtensions.dll")
from TestExtensions import TestExt
y = TestExt()
y.z = 42

But this code throws a AttributeError saying ‘TestExt’ object has no attribute ‘z’. What now? Enter Stage Right DLR’s extension mechanism. There are five methods that a .NET class can implement that have a special meaning to the binder if you tell it so. The methods are:

  • GetCustomMember – runs before normal .NET lookups
  • GetBoundMember – runs after normal .NET lookups
  • SetMember – runs before normal .NET member assignment
  • SetMemberAfter – runs after normal .NET member assignment
  • DeleteMember – runs before normal .NET operator access (there’s no .NET version – so its the only one)

A .NET class can implement these functions and mark them with a SpecialName attribute. The rule generated for binding now makes a call to Getter/Setter before and after the normal .NET binding is done. GetCustomMember/SetMember  is called first and if it returns a value that is treated as the result of the member-lookup. This overrides any .NET members that may exist. But if you return OperationFailed.Value from the function, then it will proceed with the normal lookup as well. GetBoundMember/SetMemberAfter is called if the normal binding fails – that is there is no member of the name that it is trying to bind to. So with that in mind let’s modify the .NET class and add this:

 Dictionary<string, object> dict = new Dictionary<string, object>();
[SpecialName]
public object GetBoundMember(string name)
{
    if (dict.ContainsKey(name))
        return dict[name];
    else
        return OperationFailed.Value;
}

[SpecialName]
public void SetMemberAfter(string methodName, object o)
{
    dict.Add(methodName, o);
}

Now if I try to do a y.z = 42, it works. I can assign y.z to a function as well and will be able to call y.z(). I could have overridden GetCustomMember and SetMember instead and the behaviour would be exactly the same since OperationFailed.Value is returned for member that are not in the dict but then there is this overhead involved for any .NET member lookup.

The SetMember function can choose to return a bool instead of void and in that case, the returnvalue would control whether further binding lookups happen or not.

So, what exactly is the use of all this, why would I want to do this? Consider writing an object model for something like the following xml file:

<foo>
    <bar>baz</bar>
</foo>

Now I want to access this xml as foo.bar and the value of that should be baz. To implement this all that I would need to do is add the GetBoundMember method to the .NET XmlElement class to do a search and return another XmlElement or add a extension method to XmlElement. Now here is the part where things come a little unstuck in IronPython. Extension methods dont show up in reflection, so the IronPython support for that currently isn’t where we want it to be. There is a way around though. You can decorate an assembly with an ExtensionType attribute describing what type you are extending and with what class. After that you would need to register the assembly once so that those methods get injected at the right places. This might change in the future with a better way but this works for now. This is the code you’d need to implement:

 [assembly: ExtensionType(typeof(System.Xml.XmlElement), typeof(TestExtensions.ExtClass.XmlElementExtension))]
namespace TestExtensions
{    
    public class ExtClass
    {
        static ExtClass()
        {
            Microsoft.Scripting.Runtime.RuntimeHelpers.RegisterAssembly(typeof(ExtClass).Assembly);
        }

        public static XmlElement Load(string fileName)
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(fileName);
            return doc.DocumentElement;
        }
        public static class XmlElementExtension
        {
            [SpecialName]
            public static object GetCustomMember(object myObj, string name)
            {
                XmlElement xml = myObj as XmlElement;

                if (xml != null)
                {
                    for (XmlNode n = xml.FirstChild; n != null; n = n.NextSibling)
                    {
                        if (n is XmlElement && string.CompareOrdinal(n.Name, name) == 0)
                        {
                            if (n.HasChildNodes && n.FirstChild == n.LastChild && n.FirstChild is XmlText)
                            {
                                return n.InnerText;
                            }
                            else
                            {
                                return n;
                            }

                        }
                    }
                }
                return OperationFailed.Value;
            }
        }
    }
}

Now from IronPython do this:

 import clr
clr.AddReference("TestExtensions.dll")
from TestExtensions import ExtClass
foo = ExtClass.Load("test.xml")
print foo.bar

This prints out “baz”. Sweet!

Comments

  • Anonymous
    April 12, 2008
    PingBack from http://microsoftnews.askpcdoc.com/?p=1625

  • Anonymous
    December 25, 2008
    Thanx for the blog entry. Took me forever to find on the web though :) I've been using a pre IronPython 2.0 version and used to implement ICustomAttributes; which is obviously no longer a possibility.

  • Anonymous
    March 16, 2010
    When using IronPython 2.6 RC1 for .net 2.0 and above,I don't see Microsoft.Scripting.Runtime.RuntimeHelpers - is there a different way to register extension assemblies now?

  • Anonymous
    March 22, 2010
    Extension assemblies should just be loaded into the ScriptRuntime using ScriptRuntime.LoadAssembly now.